diff --git a/.eslintignore b/.eslintignore index 84b704131..96ea5f9a1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,9 +1,12 @@ dist -types +/types +packages/*/types coverage tests babel.config.js webpack.* +webpack +jest.config.js samples \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 005f22363..0e6c48287 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,7 @@ ], "parser": "@typescript-eslint/parser", "parserOptions": { - "project": "./tsconfig.json" + "project": "./tsconfig.eslint.json" }, "plugins": [ "@typescript-eslint" @@ -23,6 +23,23 @@ } }, "rules": { + "import/no-restricted-paths": [ + "error", + { + "zones": [ + { + "target": "./core", + "from": "./packages", + "message": "core/ must not import from packages/ — the @core boundary points downward only. Move shared code into core/ instead of importing up." + }, + { + "target": "./packages/skyflow-flowvault-js", + "from": "./packages/skyflow-js", + "message": "skyflow-flowvault-js must not import from skyflow-js — the two SDK packages are independent. Share code via @core only." + } + ] + } + ], "import/no-cycle": "off", "prefer-promise-reject-errors": "off", "max-classes-per-file": [ @@ -32,5 +49,16 @@ "no-param-reassign": "off", "prefer-destructuring": "off", "@typescript-eslint/no-implied-eval": "off" - } + }, + "overrides": [ + { + "files": ["core/**/*.ts", "core/**/*.js"], + "rules": { + "import/no-extraneous-dependencies": [ + "error", + { "packageDir": ["./", "./packages/skyflow-js"] } + ] + } + } + ] } \ No newline at end of file diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 6bc8ea374..4e3914004 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -2,7 +2,11 @@ name: Public Beta Release on: push: - tags: '*.*.*-beta.*' + # The '!flowvault-*' negation excludes the flowvault namespace: a glob '*' + # matches '-', so a plain '*.*.*-beta.*' would also match a flowvault beta + # tag (flowvault-v1.0.0-beta.3) and fire this skyflow-js workflow with a + # garbage version. flowvault betas are handled by flowvault-beta-release.yml. + tags: ['*.*.*-beta.*', '!flowvault-*'] paths-ignore: - "package.json" - "package-lock.json" @@ -15,6 +19,12 @@ jobs: with: REGISTRY_URL: "https://registry.npmjs.org" TAG: "beta" + PACKAGE_DIR: "packages/skyflow-js" + PACKAGE_NAME: "skyflow-js" + DIST_DIR: "packages/skyflow-js/dist/v1" + S3_PREFIX: "" + TAG_PREFIX: "" + INVALIDATION_PATH: "/*" secrets: IFRAME_SECURE_ORIGIN: ${{ secrets.SANDBOX_IFRAME_SECURE_ORIGIN }} IFRAME_SECURE_SITE: ${{ secrets.SANDBOX_IFRAME_SECURE_SITE }} diff --git a/.github/workflows/common-release.yml b/.github/workflows/common-release.yml index b4ccb57c5..5d688082b 100644 --- a/.github/workflows/common-release.yml +++ b/.github/workflows/common-release.yml @@ -9,6 +9,39 @@ on: REGISTRY_URL: required: false type: string + # --- Package targeting (defaults preserve skyflow-js behavior) --- + PACKAGE_DIR: + required: false + type: string + default: packages/skyflow-js + PACKAGE_NAME: + required: false + type: string + default: skyflow-js + # Tag namespace for this package. Empty => bare-semver tags (skyflow-js, + # e.g. "2.7.9"); "flowvault-" => "flowvault-v1.0.0". Stripped (along with + # an optional leading "v") from the triggering tag to get the version. + TAG_PREFIX: + required: false + type: string + default: '' + DIST_DIR: + required: false + type: string + default: packages/skyflow-js/dist/v1 + # S3 key prefix placed BEFORE the version segment (e.g. "flowvault/"). + # Empty => s3://bucket/v/ (skyflow-js, unchanged). + S3_PREFIX: + required: false + type: string + default: '' + # CloudFront invalidation path. "/*" preserves current behavior; a + # package can scope it (e.g. "/flowvault/*") to avoid churning the + # other package's cache. + INVALIDATION_PATH: + required: false + type: string + default: '/*' secrets: PAT_ACTIONS: required: true @@ -42,29 +75,40 @@ jobs: token: ${{ secrets.PAT_ACTIONS }} fetch-depth: 0 - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 14.17.6 + node-version: '20.x' registry-url: ${{ inputs.REGISTRY_URL }} - run: npm install --ignore-scripts - - name: Get Previous Tag - run: | - echo "TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV - - name: Set RELEASE_VERSION run: | + PREFIX="${{ inputs.TAG_PREFIX }}" if [ "${{ inputs.TAG }}" == "internal" ]; then - echo "RELEASE_VERSION=${{ env.TAG }}-dev.$(git rev-parse --short $GITHUB_SHA)" >> $GITHUB_ENV + if [ -n "$PREFIX" ]; then + PREV_TAG=$(git describe --tags --abbrev=0 --match "${PREFIX}*" 2>/dev/null || true) + else + PREV_TAG=$(git describe --tags --abbrev=0 --match '[0-9]*' --exclude 'flowvault-*' 2>/dev/null || true) + fi + if [ -z "$PREV_TAG" ]; then + BASE_VERSION=$(node -p "require('./${{ inputs.PACKAGE_DIR }}/package.json').version") + else + BASE_VERSION="${PREV_TAG#$PREFIX}" + BASE_VERSION="${BASE_VERSION#v}" + fi + echo "RELEASE_VERSION=${BASE_VERSION}-dev.$(git rev-parse --short $GITHUB_SHA)" >> $GITHUB_ENV else - echo "RELEASE_VERSION=${{ env.TAG }}" >> $GITHUB_ENV + BASE_VERSION="${{ github.ref_name }}" + BASE_VERSION="${BASE_VERSION#$PREFIX}" + BASE_VERSION="${BASE_VERSION#v}" + echo "RELEASE_VERSION=${BASE_VERSION}" >> $GITHUB_ENV fi - name: Bump Version run: | chmod +x ./scripts/bump_version.sh - ./scripts/bump_version.sh "${{ env.RELEASE_VERSION }}" + ./scripts/bump_version.sh "${{ env.RELEASE_VERSION }}" "${{ inputs.PACKAGE_DIR }}" - name: Resolve Branch for the Tagged Commit id: resolve-branch @@ -83,25 +127,26 @@ jobs: echo "branch_name=$BRANCH_NAME" >> $GITHUB_ENV - name: Commit Changes + if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} run: | git config user.name ${{ github.actor }} git config user.email ${{ github.actor }}@users.noreply.github.com git checkout ${{ env.branch_name }} - git add package.json + git add ${{ inputs.PACKAGE_DIR }}/package.json git commit -m "[AUTOMATED] Release - ${{ env.RELEASE_VERSION }}" git push origin ${{ env.branch_name }} -f - name: Build Browser SDK - run: npm run build-browser-sdk + run: npm run build-browser-sdk -w ${{ inputs.PACKAGE_NAME }} env: IFRAME_SECURE_ORIGIN: ${{ secrets.IFRAME_SECURE_ORIGIN }} - IFRAME_SECURE_SITE: "v${{ env.RELEASE_VERSION }}/${{ secrets.IFRAME_SECURE_SITE }}" + IFRAME_SECURE_SITE: "${{ inputs.S3_PREFIX }}v${{ env.RELEASE_VERSION }}/${{ secrets.IFRAME_SECURE_SITE }}" - name: Build Iframe - run: npm run build-iframe + run: npm run build-iframe -w ${{ inputs.PACKAGE_NAME }} env: IFRAME_SECURE_ORIGIN: ${{ secrets.IFRAME_SECURE_ORIGIN }} - IFRAME_SECURE_SITE: "v${{ env.RELEASE_VERSION }}/${{ secrets.IFRAME_SECURE_SITE }}" + IFRAME_SECURE_SITE: "${{ inputs.S3_PREFIX }}v${{ env.RELEASE_VERSION }}/${{ secrets.IFRAME_SECURE_SITE }}" - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 @@ -110,49 +155,49 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} - # - name: Set Latest Tag - # run: | - # mkdir tags - # echo "v${{ env.RELEASE_VERSION }}" > tags/latest - - # - name: Deploy Latest Tag to S3 - # run: aws s3 cp --recursive tags s3://${{ secrets.AWS_BUCKET_NAME }}/ - - # - name: Remove Latest Tag - # run: rm -rf tags - - name: Deploy to S3 run: | - aws s3 cp --recursive dist/v1 s3://${{ secrets.AWS_BUCKET_NAME }}/v${{ env.RELEASE_VERSION }}/ + aws s3 cp --recursive ${{ inputs.DIST_DIR }} s3://${{ secrets.AWS_BUCKET_NAME }}/${{ inputs.S3_PREFIX }}v${{ env.RELEASE_VERSION }}/ + - name: Update latest pointer in S3 + run: | + printf 'v%s' "${{ env.RELEASE_VERSION }}" > latest + aws s3 cp latest \ + "s3://${{ secrets.AWS_BUCKET_NAME }}/${{ inputs.S3_PREFIX }}latest" \ + --content-type text/plain \ + --cache-control no-cache + rm -f latest - name: Build Node SDK run: | - npm run build:types - npm run build-node-sdk + npm run build:types -w ${{ inputs.PACKAGE_NAME }} + npm run build-node-sdk -w ${{ inputs.PACKAGE_NAME }} env: IFRAME_SECURE_ORIGIN: ${{ secrets.IFRAME_SECURE_ORIGIN }} - IFRAME_SECURE_SITE: "v${{ env.RELEASE_VERSION }}/${{ secrets.IFRAME_SECURE_SITE }}" + IFRAME_SECURE_SITE: "${{ inputs.S3_PREFIX }}v${{ env.RELEASE_VERSION }}/${{ secrets.IFRAME_SECURE_SITE }}" - name: Publish SDK run: | if [ "${{ inputs.TAG }}" == "beta" ]; then - npm publish --tag v${{ env.RELEASE_VERSION }} + npm publish -w ${{ inputs.PACKAGE_NAME }} --tag beta elif [ "${{ inputs.TAG }}" == "internal" ]; then - curl -u ${{ secrets.JFROG_USERNAME }}:${{ secrets.JFROG_PASSWORD }} https://prekarilabs.jfrog.io/prekarilabs/api/npm/auth/ > ~/.npmrc + JFROG_AUTH=$(echo -n "$JFROG_USERNAME:$JFROG_PASSWORD" | base64 -w 0) + echo "::add-mask::${JFROG_AUTH}" npm config set registry https://prekarilabs.jfrog.io/prekarilabs/api/npm/npm/ - npm config set unsafe-perm true - npm publish + npm config set //prekarilabs.jfrog.io/prekarilabs/api/npm/npm/:_auth "$JFROG_AUTH" + npm publish -w ${{ inputs.PACKAGE_NAME }} else - npm publish + npm publish -w ${{ inputs.PACKAGE_NAME }} fi env: NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + JFROG_USERNAME: ${{ secrets.JFROG_USERNAME }} + JFROG_PASSWORD: ${{ secrets.JFROG_PASSWORD }} - name: Invalidate CloudFront cache 10 times run: | for i in {1..10}; do aws cloudfront create-invalidation \ --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \ - --paths "/*" + --paths "${{ inputs.INVALIDATION_PATH }}" sleep 10 done diff --git a/.github/workflows/flowvault-beta-release.yml b/.github/workflows/flowvault-beta-release.yml new file mode 100644 index 000000000..5ebc96467 --- /dev/null +++ b/.github/workflows/flowvault-beta-release.yml @@ -0,0 +1,40 @@ +name: FlowVault Public Beta Release + +# Release entry point for skyflow-flowvault-js (flowDB). Triggered by a +# flowvault-namespaced beta tag so it never collides with skyflow-js's bare +# semver tags. Calls the shared reusable workflow with flowvault's package +# targeting (S3_PREFIX/TAG_PREFIX/INVALIDATION_PATH), landing in the SAME +# sandbox bucket + CloudFront distribution under the flowvault/ prefix. +on: + push: + tags: 'flowvault-v*.*.*-beta.*' + paths-ignore: + - "package.json" + - "package-lock.json" + - "*.md" + + +jobs: + flowvault-beta-release: + uses: ./.github/workflows/common-release.yml + with: + REGISTRY_URL: "https://registry.npmjs.org" + TAG: "beta" + PACKAGE_DIR: "packages/skyflow-flowvault-js" + PACKAGE_NAME: "skyflow-flowvault-js" + DIST_DIR: "packages/skyflow-flowvault-js/dist/v1" + S3_PREFIX: "flowvault/" + TAG_PREFIX: "flowvault-" + INVALIDATION_PATH: "/*" + secrets: + # Same sandbox bucket / distribution / CDN origin as skyflow-js — flowDB + # just lands under the flowvault/ key prefix, so the shared SANDBOX_* + IFRAME_SECURE_ORIGIN: ${{ secrets.SANDBOX_IFRAME_SECURE_ORIGIN }} + IFRAME_SECURE_SITE: ${{ secrets.SANDBOX_IFRAME_SECURE_SITE }} + AWS_ACCESS_KEY_ID: ${{ secrets.SANDBOX_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.SANDBOX_AWS_ACCESS_KEY_SECRET }} + AWS_REGION: ${{ secrets.SANDBOX_AWS_REGION }} + AWS_BUCKET_NAME: ${{ secrets.SANDBOX_AWS_BUCKET_NAME }} + CF_DISTRIBUTION_ID: ${{ secrets.SANDBOX_CF_DISTRIBUTION_ID }} + PAT_ACTIONS: ${{ secrets.PAT_ACTIONS }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/flowvault-release.yml b/.github/workflows/flowvault-release.yml new file mode 100644 index 000000000..2da8156dc --- /dev/null +++ b/.github/workflows/flowvault-release.yml @@ -0,0 +1,42 @@ +name: FlowVault Public Release + +# Production release entry point for skyflow-flowvault-js (flowDB). Triggered by +# a flowvault-namespaced release tag. The '!...-beta*' negation guard ensures a +# beta tag (flowvault-v1.0.0-beta.3) does NOT also fire this prod workflow — +# only clean release tags (flowvault-v1.0.0) do. Lands in the SAME prod bucket + +# CloudFront distribution as skyflow-js, under the flowvault/ prefix. +on: + push: + tags: + - 'flowvault-v*' + - '!flowvault-v*-beta*' + paths-ignore: + - "package.json" + - "package-lock.json" + - "*.md" + + +jobs: + flowvault-public-release: + uses: ./.github/workflows/common-release.yml + with: + REGISTRY_URL: "https://registry.npmjs.org" + TAG: "public" + PACKAGE_DIR: "packages/skyflow-flowvault-js" + PACKAGE_NAME: "skyflow-flowvault-js" + DIST_DIR: "packages/skyflow-flowvault-js/dist/v1" + S3_PREFIX: "flowvault/" + TAG_PREFIX: "flowvault-" + INVALIDATION_PATH: "/*" + secrets: + # Same prod bucket / distribution / CDN origin as skyflow-js — flowDB just + # lands under the flowvault/ key prefix, so the shared PROD_* secrets are + IFRAME_SECURE_ORIGIN: ${{ secrets.PROD_IFRAME_SECURE_ORIGIN }} + IFRAME_SECURE_SITE: ${{ secrets.PROD_IFRAME_SECURE_SITE }} + AWS_ACCESS_KEY_ID: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.PROD_AWS_ACCESS_KEY_SECRET }} + AWS_REGION: ${{ secrets.PROD_AWS_REGION }} + AWS_BUCKET_NAME: ${{ secrets.PROD_AWS_BUCKET_NAME }} + CF_DISTRIBUTION_ID: ${{ secrets.PROD_CF_DISTRIBUTION_ID }} + PAT_ACTIONS: ${{ secrets.PAT_ACTIONS }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index 10f226403..ea105d9a8 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -11,11 +11,63 @@ on: jobs: - internal-release: + # Classify what this release branch changes relative to main, then fan out to + # one or both packages: + # core/ (or any shared root) changed -> publish BOTH + # only packages/skyflow-js changed -> publish skyflow-js + # only packages/skyflow-flowvault-js -> publish flowvault + detect-changes: + runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[AUTOMATED] Release')" + outputs: + js: ${{ steps.filter.outputs.js }} + flowvault: ${{ steps.filter.outputs.flowvault }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changed scopes + id: filter + run: | + git fetch --no-tags origin main + BASE=$(git merge-base FETCH_HEAD HEAD) + echo "Merge-base with main: $BASE" + + CHANGED=$(git diff --name-only "$BASE" HEAD) + echo "Changed files:" + echo "$CHANGED" + + # Shared roots feed BOTH package builds, so treat them like core. + SHARED='^(core/|webpack/|scripts/|package-lock\.json|package\.json|\.github/workflows/)' + JS='^packages/skyflow-js/' + FLOWVAULT='^packages/skyflow-flowvault-js/' + + js=false + flowvault=false + if echo "$CHANGED" | grep -qE "$SHARED"; then + js=true + flowvault=true + fi + if echo "$CHANGED" | grep -qE "$JS"; then js=true; fi + if echo "$CHANGED" | grep -qE "$FLOWVAULT"; then flowvault=true; fi + + echo "Resolved: js=$js flowvault=$flowvault" + echo "js=$js" >> "$GITHUB_OUTPUT" + echo "flowvault=$flowvault" >> "$GITHUB_OUTPUT" + + internal-release-js: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.js == 'true' }} uses: ./.github/workflows/common-release.yml with: TAG: "internal" + PACKAGE_DIR: "packages/skyflow-js" + PACKAGE_NAME: "skyflow-js" + DIST_DIR: "packages/skyflow-js/dist/v1" + S3_PREFIX: "" + TAG_PREFIX: "" + INVALIDATION_PATH: "/*" secrets: IFRAME_SECURE_ORIGIN: ${{ secrets.IFRAME_SECURE_ORIGIN }} IFRAME_SECURE_SITE: ${{ secrets.IFRAME_SECURE_SITE }} @@ -28,3 +80,28 @@ jobs: JFROG_PASSWORD: ${{ secrets.JFROG_PASSWORD }} PAT_ACTIONS: ${{ secrets.PAT_ACTIONS }} + internal-release-flowvault: + needs: detect-changes + if: ${{ needs.detect-changes.outputs.flowvault == 'true' }} + uses: ./.github/workflows/common-release.yml + with: + TAG: "internal" + PACKAGE_DIR: "packages/skyflow-flowvault-js" + PACKAGE_NAME: "skyflow-flowvault-js" + DIST_DIR: "packages/skyflow-flowvault-js/dist/v1" + S3_PREFIX: "flowvault/" + TAG_PREFIX: "flowvault-" + INVALIDATION_PATH: "/*" + secrets: + # Same internal/BLITZ bucket + distribution + CDN origin as skyflow-js; + # flowDB just lands under the flowvault/ key prefix. + IFRAME_SECURE_ORIGIN: ${{ secrets.IFRAME_SECURE_ORIGIN }} + IFRAME_SECURE_SITE: ${{ secrets.IFRAME_SECURE_SITE }} + AWS_ACCESS_KEY_ID: ${{ secrets.BLITZ_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.BLITZ_AWS_ACCESS_KEY_SECRET }} + AWS_REGION: ${{ secrets.BLITZ_AWS_REGION }} + AWS_BUCKET_NAME: ${{ secrets.BLITZ_AWS_BUCKET_NAME }} + CF_DISTRIBUTION_ID: ${{ secrets.BLITZ_CF_DISTRIBUTION_ID }} + JFROG_USERNAME: ${{ secrets.JFROG_USERNAME }} + JFROG_PASSWORD: ${{ secrets.JFROG_PASSWORD }} + PAT_ACTIONS: ${{ secrets.PAT_ACTIONS }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5e041ac2c..cbe48fc93 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,19 +10,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: - node-version: 14.17.6 + node-version: 18 - name: install modules run: npm ci --ignore-scripts + - name: Type check + run: npm run type-check + - name: Run tests run: npm run test - - - name: Codecov + + - name: Codecov (skyflow-js) uses: codecov/codecov-action@v2.1.0 with: token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} - files: coverage/lcov.info + files: packages/skyflow-js/coverage/lcov.info name: codecov-skyflow-js + verbose: true + + - name: Codecov (skyflow-flowvault-js) + uses: codecov/codecov-action@v2.1.0 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: packages/skyflow-flowvault-js/coverage/lcov.info + name: codecov-skyflow-flowvault-js verbose: true \ No newline at end of file diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7551cd72a..ba18db73b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,26 +25,37 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v4 with: - node-version: 14.17.6 + node-version: 18 - name: install modules run: npm ci --ignore-scripts - name: Check code quality uses: stefanoeb/eslint-action@1.0.2 + - name: Type check + run: npm run type-check + - name: Run tests run: npm run test - - - name: Codecov + + - name: Codecov (skyflow-js) uses: codecov/codecov-action@v2.1.0 with: token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} - files: coverage/lcov.info + files: packages/skyflow-js/coverage/lcov.info name: codecov-skyflow-js verbose: true - + + - name: Codecov (skyflow-flowvault-js) + uses: codecov/codecov-action@v2.1.0 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: packages/skyflow-flowvault-js/coverage/lcov.info + name: codecov-skyflow-flowvault-js + verbose: true + - name: Browser Build run: npm run build-browser-sdk env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 882b0dff2..a11e3acd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,10 @@ name: Public Release on: push: - tags: '*.*.\d+' + tags: + - '*.*.\d+' + - '!flowvault-*' + - '!*-beta*' paths-ignore: - "package.json" - "package-lock.json" @@ -15,6 +18,12 @@ jobs: with: REGISTRY_URL: "https://registry.npmjs.org" TAG: "public" + PACKAGE_DIR: "packages/skyflow-js" + PACKAGE_NAME: "skyflow-js" + DIST_DIR: "packages/skyflow-js/dist/v1" + S3_PREFIX: "" + TAG_PREFIX: "" + INVALIDATION_PATH: "/*" secrets: IFRAME_SECURE_ORIGIN: ${{ secrets.PROD_IFRAME_SECURE_ORIGIN }} IFRAME_SECURE_SITE: ${{ secrets.PROD_IFRAME_SECURE_SITE }} diff --git a/.gitignore b/.gitignore index a096ec04d..3b8d0cd3a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ dist testing coverage -types +# Generated declaration output only (tsc outDir). Anchored so it does not also +# ignore the core/types/ source folder. Each package emits into its own types/. +/types +packages/*/types diff --git a/README.md b/README.md index 814822d31..c4b24ff06 100644 --- a/README.md +++ b/README.md @@ -1,4506 +1,24 @@ # skyflow-js -Skyflow's JavaScript SDK can be used to securely collect, tokenize, and reveal sensitive data in the browser without exposing your front-end infrastructure to sensitive data. ---- +Skyflow's JavaScript SDKs let you securely collect, tokenize, and reveal sensitive data in the browser without exposing your front-end infrastructure to sensitive data. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-js/actions) -[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-js.svg)](https://www.npmjs.com/package/skyflow-js) -[![License](https://img.shields.io/github/license/skyflowapi/skyflow-android)](https://github.com/skyflowapi/skyflow-js/blob/main/LICENSE) +[![License](https://img.shields.io/github/license/skyflowapi/skyflow-js)](https://github.com/skyflowapi/skyflow-js/blob/main/LICENSE) -## Browsers support +This repository publishes two SDKs from a shared codebase. Pick the one that matches your vault type: -| IE / Edge
IE / Edge | Firefox
Firefox | Chrome
Chrome | Safari
Safari -|--------------------------------------------------------------------------------------------------------------------------------------------------------------| --------- | --------- |-------------------------------------------------------------------------------------------------------------------------------------------------------| -# Table of Contents -- [**Including Skyflow.js**](#including-skyflowjs) -- [**Initializing Skyflow.js**](#initializing-skyflowjs) -- [**Securely collecting data client-side**](#securely-collecting-data-client-side) -- [**Securely collecting data client-side using Composable Elements**](#securely-collecting-data-client-side-using-composable-elements) -- [**Securely revealing data client-side**](#securely-revealing-data-client-side) -- [**Securely deleting data client-side**](#securely-deleting-data-client-side) -- [**Set Custom Network messages on container**](#set-custom-network-messages-on-container) ---- +| SDK | Vault type | Install (npm / script tag) | Import | Documentation | +|-----|------------|----------------------------|--------|---------------| +| **skyflow-js** | PDB vault (v1 API) | `npm i skyflow-js` / ` -``` - - -Using npm - -``` -npm install skyflow-js -``` - ---- - -# Initializing Skyflow.js -Use the `init()` method to initialize a Skyflow client as shown below. -```javascript -import Skyflow from 'skyflow-js' // If using script tag, this line is not required. - -const skyflowClient = Skyflow.init({ - vaultID: 'string', // Id of the vault that the client should connect to. - vaultURL: 'string', // URL of the vault that the client should connect to. - getBearerToken: helperFunc, // Helper function that retrieves a Skyflow bearer token from your backend. - options: { - logLevel: Skyflow.LogLevel, // Optional, if not specified default is ERROR. - env: Skyflow.Env // Optional, if not specified default is PROD. - } -}); -``` -For the `getBearerToken` parameter, pass in a helper function that retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. A sample implementation is shown below: - -For example, if the response of the consumer tokenAPI is in the below format - -``` -{ - "accessToken": string, - "tokenType": string -} - -``` -then, your getBearerToken Implementation should be as below - -```javascript -const getBearerToken = () => { - return new Promise((resolve, reject) => { - const Http = new XMLHttpRequest(); - - Http.onreadystatechange = () => { - if (Http.readyState === 4) { - if (Http.status === 200) { - const response = JSON.parse(Http.responseText); - resolve(response.accessToken); - } else { - reject('Error occured'); - } - } - }; - - Http.onerror = error => { - reject('Error occured'); - }; - - const url = 'https://api.acmecorp.com/skyflowToken'; - Http.open('GET', url); - Http.send(); - }); -}; - -``` -For `logLevel` parameter, there are 4 accepted values in Skyflow.LogLevel - -- `DEBUG` - - When `Skyflow.LogLevel.DEBUG` is passed, all level of logs will be printed(DEBUG, INFO, WARN, ERROR). - -- `INFO` - - When `Skyflow.LogLevel.INFO` is passed, INFO logs for every event that has occurred during the SDK flow execution will be printed along with WARN and ERROR logs. - - -- `WARN` - - When `Skyflow.LogLevel.WARN` is passed, WARN and ERROR logs will be printed. - -- `ERROR` - - When `Skyflow.LogLevel.ERROR` is passed, only ERROR logs will be printed. - -`Note`: - - The ranking of logging levels is as follows : DEBUG < INFO < WARN < ERROR - - since `logLevel` is optional, by default the logLevel will be `ERROR`. - - - -For `env` parameter, there are 2 accepted values in Skyflow.Env - -- `PROD` -- `DEV` - - In [Event Listeners](#event-listener-on-collect-elements), actual value of element can only be accessed inside the handler when the `env` is set to `DEV`. - -`Note`: - - since `env` is optional, by default the env will be `PROD`. - - Use `env` option with caution, make sure the env is set to `PROD` when using `skyflow-js` in production. - ---- - -# Securely collecting data client-side -- [**Insert data into the vault**](#insert-data-into-the-vault) -- [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) -- [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) -- [**Bin lookup**](#bin-lookup) -- [**Using validations on Collect Elements**](#validations) -- [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) -- [**UI Error for Collect Elements**](#ui-error-for-collect-elements) -- [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) -- [**Update Collect Elements**](#update-collect-elements) -- [**Using Skyflow File Element to upload a file**](#using-skyflow-file-element-to-upload-a-file) - -## Insert data into the vault - -To insert data into the vault, use the `insert(records, options?)` method of the Skyflow client. The `records` parameter takes a JSON object of the records to insert into the below format. The `options` parameter takes an object of optional parameters for the insertion. The `insert` method also supports upsert operations. - -```javascript -const records = { - records: [ - { - table: 'string', // Table into which record should be inserted. - fields: { - column1: 'value', // Column names should match vault column names. - //...additional fields here - }, - }, - // ...additional records here. - ], -}; - -const options = { - tokens: true, // Indicates whether or not tokens should be returned for the inserted data. Defaults to 'true' - upsert: [ // Upsert operations support in the vault - { - table: 'string', // Table name - column: 'value', // Unique column in the table - } - ] -} - -skyflowClient.insert(records, options); -``` - -An [example](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/pure-js.html) of an insert call: -```javascript -skyflowClient.insert({ - records: [ - { - table: 'cards', - fields: { - cardNumber: '41111111111', - cvv: '123', - }, - }, - ], -}); -``` - -The sample response: -```javascript -{ - "records": [ - { - "table": "cards", - "fields":{ - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - } - ] -} -``` - -## Update data in the vault - -To update data in the vault by skyflowID, use the `update(request, options?)` method of the Skyflow client. The request object is a JSON object describing the data to update, including the `table`, `fields`, and the `skyflowID` of the record to update. The options parameter takes an object of optional parameters for the update and includes an option to return tokenized data for the updated fields. - -```javascript -const updateRecord = { - table: 'string', // Table in which record should be updated. - fields: { - column1: 'value', // Fields to update. Column names should match vault column names. - //...additional fields here - }, - skyflowID: 'string', // The skyflow_id of the record to update. -}; - -const options = { - tokens: true, // Indicates whether or not tokens should be returned for the updated data. Defaults to 'true' -}; - -skyflowClient.update(updateRecord, options); -``` - -An [example](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/pure-update.html) of update call: -```javascript -skyflowClient.update({ - table: 'cards', - fields: { - cardNumber: '41111111111', - cvv: '123', - }, - skyflowID: '43127a6c-5c15-4513-aa15-29f50bb37182' -}); -``` - -The sample response: - -```javascript -{ - "updatedField": { - "skyflowID": "43127a6c-5c15-4513-aa15-29f50bb37182", - "cardNumber": "f390186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0d1a5" - } -} -``` - -**Note**: -- The `skyflowID` field is required and should be the Skyflow ID of the record you want to update. -- If tokens is set to true, the response will include tokens for the updated fields. - -## Using Skyflow Elements to collect data - -**Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. These elements are hosted by Skyflow and injected into your web page as iFrames. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements on your web page. - -### Step 1: Create a container - -First create a container for the form elements using the `container(Skyflow.ContainerType)` method of the Skyflow client as show below: - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) -``` - -### Step 2: Create a collect Element - -A Skyflow collect Element is defined as shown below: - -```javascript -const collectElement = { - table: 'string', // Required, the table this data belongs to. - column: 'string', // Required, the column into which this data should be inserted. - type: Skyflow.ElementType, // Skyflow.ElementType enum. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. - label: 'string', // Optional, label for the form element. - placeholder: 'string', // Optional, placeholder for the form element. - altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. - validations: [], // Optional, array of validation rules. -} -``` -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. - -**Note**: -- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) - -The `inputStyles` field accepts a style object which consists of CSS properties that should be applied to the form element in the following states: -* `base`: all variants inherit from these styles -* `complete`: applied when the Element has valid input -* `empty`: applied when the Element has no input -* `focus`: applied when the Element has focus -* `invalid`: applied when the Element has invalid input -* `cardIcon`: applied to the card type icon in CARD_NUMBER Element -* `copyIcon`: applied to copy icon in Elements when enableCopy option is true -* `global`: used for global styles like font-family. - -Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1). - -An example of a inputStyles object: -```javascript -inputStyles: { - base: { - border: '1px solid #eae8ee', - padding: '10px 16px', - borderRadius: '4px', - color: '#1d1d1d', - '&:hover': { // Hover styles. - borderColor: 'green' - }, - fontFamily: '"Roboto", sans-serif' - }, - complete: { - color: '#4caf50', - }, - empty: {}, - focus: {}, - invalid: { - color: '#f44336', - }, - cardIcon: { - position: 'absolute', - left: '8px', - bottom: 'calc(50% - 12px)', - }, - copyIcon: { - position: 'absolute', - right: '8px', - }, - global: { - '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` -The states that are available for `labelStyles` are `base`, `focus`, `global` and `requiredAsterisk`. -* `requiredAsterisk`: styles applied for the Asterisk symbol in the label. - -An example of a labelStyles object: - -```javascript -labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - fontFamily: '"Roboto", sans-serif' - }, - focus: { - color: '#1d1d1d', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - requiredAsterisk:{ - color: 'red' - } -}, -``` - -The state that is available for `errorTextStyles` are `base` and `global`, it shows up when there is some error in the collect element. - -An example of a errorTextStyles object: - -```javascript -errorTextStyles: { - base: { - color: '#f44336', - fontFamily: '"Roboto", sans-serif' - }, - global: { - '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -Finally, the `type` field takes a Skyflow ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 8 types: -- `CARDHOLDER_NAME` -- `CARD_NUMBER` -- `EXPIRATION_DATE` -- `EXPIRATION_MONTH` -- `EXPIRATION_YEAR` -- `CVV` -- `INPUT_FIELD` -- `PIN` -- `FILE_INPUT` - - -The `INPUT_FIELD` type is a custom UI element without any built-in validations. For information on validations, see [validations](#validations). - -Along with CollectElement we can define other options which takes a object of optional parameters as described below: - -```javascript -const options = { - required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. - enableCardIcon: true, // Optional, indicates whether a card icon should be enabled (only applicable for CARD_NUMBER ElementType). - enableCopy: false, // Optional, enables the copy icon to collect elements to copy text to clipboard. Defaults to 'false'). - format: String, // Optional, format for the element - translation: {}, // Optional, indicates the allowed data type value for format. - cardMetadata: {}, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). - masking: true, // Optional, indicates whether the input should be masked. Defaults to 'false'. - maskingChar: '*', // Optional, character used for masking input when masking is enabled. Defaults to '*'. -}; -``` - -`required`: Indicates whether the field is marked as required or not. If not provided, it defaults to false. - -`enableCardIcon` : Indicates whether the icon is visible for the CARD_NUMBER element. Defaults to true. - -`enableCopy` : Indicates whether the copy icon is visible in collect and reveal elements. - -`format`: A string value that indicates the format pattern applicable to the element type. -Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. - - For INPUT_FIELD elements, - - the length of `format` determines the expected length of the user input. - - if `translation` isn't specified, the `format` value is considered a string literal. - -`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. - -Accepted values by element type: - -| Element type | `format`and `translation` values | Examples | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | -| EXPIRATION_DATE |
  • `format`
  • | | -| EXPIRATION_YEAR |
  • `format`
  • | | -| CARD_NUMBER |
  • `format`
  • | | -| INPUT_FIELD |
  • `format`: A string that matches the desired output, with placeholder characters of your choice.
  • `translation`: An object of key/value pairs. Defaults to `{"X": "[0-9]"}`
  • | With a `format` of `+91 XXXX-XX-XXXX` and a `translation` of `[ "X": "[0-9]"]`, user input of "1234121234" displays as "+91 1234-12-1234". | - -`cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow accept card types based on which SDK will display card brand choice dropdown in the card number element. `Skyflow.CardType` is an enum with all skyflow supported card schemes. - -```javascript -import Skyflow from 'skyflow-js' - -const cardMetadata = { - scheme: Skyflow.CardType [] // Optional, array of skyflow supported card types. -} -``` - -
    Supported card types by Skyflow.CardType :
    - -- `VISA` -- `MASTERCARD` -- `AMEX` -- `DINERS_CLUB` -- `DISCOVER` -- `JCB` -- `MAESTRO` -- `UNIONPAY` -- `HIPERCARD` -- `CARTES_BANCAIRES` - -**Collect Element Options examples for INPUT_FIELD** -Example 1 -```js -const options = { - required: true, - enableCardIcon: true, - format:'+91 XXXX-XX-XXXX', - translation: { 'X': '[0-9]' } -} -``` - -User input: "1234121234" -Value displayed in INPUT_FIELD: "+91 1234-12-1234" - -Example 2 -```js -const options = { - required: true, - enableCardIcon: true, - format: 'AY XX-XXX-XXXX', - translation: { 'X': '[0-9]', 'Y': '[A-Z]' } -} -``` - -User input: "B1234121234" -Value displayed in INPUT_FIELD: "AB 12-341-2123" - -`masking` : A boolean value for whether to mask the input of the element. When masking is enabled, user input will be replaced with a masking character. -The default masking character is `*`, but you can customize masking character using the maskingChar property. - -`maskingChar`: A single character used to mask the input when masking is enabled. Defaults to `*`, but can be customized to any character of your choice. - -Collect Element Options examples with masking: - -Example for CVV: -```js -const options = { - required: true, - enableCopy: false, - masking: true, - maskingChar: '•', -} -``` -User input: "1234" -Value displayed in CVV: "••••" - -Example for CARDHOLDER_NAME: -```js -const options = { - required: true, - enableCopy: false, - masking: true, -} -``` -User input: "John Doe" -Value displayed in CARDHOLDER_NAME: "********" - -Example for CARD_NUMBER: -```js -const options = { - required: true, - enableCopy: false, - masking: true, - maskingChar: '#' -} -``` -User input: "4111 1111 1111 1111" -Value displayed in CARD_NUMBER: "#### #### #### ####" - -Example for PIN: -```js -const options = { - required: true, - enableCopy: false, - masking: true, - maskingChar: '&' -} -``` -User input: "98364721" -Value displayed in PIN: "&&&&&&&&" - -**Note**: -- Unmasked data will be stored in the vault. - -Once the Element object and options has been defined, add it to the container using the `create(element, options)` method as shown below. The `element` param takes a Skyflow Element object and options as defined above: - -```javascript -const collectElement = { - table: 'string', // Required, the table this data belongs to. - column: 'string', // Required, the column into which this data should be inserted. - type: Skyflow.ElementType, // Skyflow.ElementType enum. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. - label: 'string', // Optional, label for the form element. - placeholder: 'string', // Optional, placeholder for the form element. - altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. - validations: [], // Optional, array of validation rules. -} - -const options = { - required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. - enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType). - enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). - format: String, // Optional, format for the element - translation: {}, // Optional, indicates the allowed data type value for format. -}; - -const element = container.create(collectElement, options); -``` - -### Step 3: Mount Elements to the DOM - -To specify where the Elements will be rendered on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has 4 empty divs with unique ids as placeholders for 4 Skyflow Elements. - -```html -
    -
    -
    -
    -
    -
    -
    -
    - - -``` - -Now, when the `mount(domElement)` method of the Element is called, the Element will be inserted in the specified div. For instance, the call below will insert the Element into the div with the id "#cardNumber". - -```javascript -element.mount('#cardNumber'); -``` -you can use the `unmount` method to reset any collect element to it's initial state. -```javascript -element.unmount(); -``` - -### Step 4: Collect data from Elements - -When the form is ready to be submitted, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: - -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format as described in the above [Insert data into vault](#insert-data-into-the-vault) section. -- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. - -```javascript -const options = { - tokens: true, // Optional, indicates whether tokens for the collected data should be returned. Defaults to 'true'. - additionalFields: { - records: [ - { - table: 'string', // Table into which record should be inserted. - fields: { - column1: 'value', // Column names should match vault column names. - // ...additional fields here. - }, - }, - // ...additional records here. - ], - }, // Optional - upsert: [ // Upsert operations support in the vault - { - table: 'string', // Table name - column: 'value', // Unique column in the table - }, - ], // Optional -}; - -container.collect(options); -``` - -### End to end example of collecting data with Skyflow Elements - -**[Sample Code:](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/skyflow-elements.html)** - -```javascript -//Step 1 -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -//Step 2 -const element = container.create({ - table: 'cards', - column: 'cardNumber', - inputstyles: { - base: { - color: '#1d1d1d', - }, - cardIcon: { - position: 'absolute', - left: '8px', - bottom: 'calc(50% - 12px)', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'Card Number', - label: 'card_number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -// Step 3 -element.mount('#cardNumber'); // Assumes there is a div with id='#cardNumber' in the webpage. - -// Step 4 - -const nonPCIRecords = { - records: [ - { - table: 'cards', - fields: { - gender: 'MALE', - }, - }, - ], -}; - -container.collect({ - tokens: true, - additionalFields: nonPCIRecords, -}); - -``` - -**Sample Response :** -```javascript -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e" - } - } - ] -} -``` -### Insert call example with upsert support -**Sample Code** - - ```javascript -//Step 1 -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) - -//Step 2 -const cardNumberElement = container.create({ - table: 'cards', - column: 'card_number', - inputStyles: { - base: { - color: '#1d1d1d', - }, - cardIcon:{ - position: 'absolute', - left:'8px', - bottom:'calc(50% - 12px)' - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold' - } - }, - errorTextStyles: { - base: { - color: '#f44336' - } - }, - placeholder: 'Card Number', - label: 'card_number', - type: Skyflow.ElementType.CARD_NUMBER -}) - - -const cvvElement = container.create({ - table: 'cards', - column: 'cvv', - inputStyles: { - base: { - color: '#1d1d1d', - }, - cardIcon:{ - position: 'absolute', - left:'8px', - bottom:'calc(50% - 12px)' - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold' - } - }, - errorTextStyles: { - base: { - color: '#f44336' - } - }, - placeholder: 'CVV', - label: 'cvv', - type: Skyflow.ElementType.CVV -}) - -// Step 3 -cardNumberElement.mount('#cardNumber') //Assumes there is a div with id='#cardNumber' in the webpage. -cvvElement.mount('#cvv'); //Assumes there is a div with id='#cvv' in the webpage. - -// Step 4 - container.collect({ - tokens: true, - upsert: [ - { - table: 'cards', - column: 'card_number', - } - ] -}) - ``` - **Skyflow returns tokens for the record you just inserted.** -```javascript -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e" - } - } - ] -} -``` - -## BIN Lookup - -Skyflow supports BIN (Bank Identification Number) lookup to help identify co-badged cards and enable card network selection. - -**What is BIN Lookup?** -A Bank Identification Number (BIN) represents the first 8 digits of a card number and identifies the issuing bank, card scheme, and country. -For co-badged cards, merchants are required to offer consumers a choice of which network to process the payment through. -You can use Skyflow’s BIN Lookup API to detect such cards and provide the appropriate options to users. - -### Example: Calling the BIN Lookup API -```javascript -// Function to call Skyflow's BIN Lookup API -const binLookup = (bin) => { - const myHeaders = new Headers(); - myHeaders.append("X-skyflow-authorization", ""); // TODO: replace bearer token - myHeaders.append("Content-Type", "application/json"); - - const raw = JSON.stringify({ - "BIN": bin - }); - - const requestOptions = { - method: "POST", - headers: myHeaders, - body: raw, - redirect: "follow" - }; - - // TODO: replace with your Skyflow vault URL - return fetch(`${VAULT_URL}/v1/card_lookup`, requestOptions); -}; -``` - -**Sample Response :** -```javascript -{ - "cards_data": [ - { - "BIN": "54284800", - "issuer_name": "CREDIT MUTUEL ARKEA", - "country_code": "FR", - "currency": "", - "card_type": "Credit", - "card_category": "", - "card_scheme": "CARTES BANCAIRES" - }, - { - "BIN": "54284800", - "issuer_name": "Credit Mutuel Arkea", - "country_code": "FR", - "currency": "", - "card_type": "Credit", - "card_category": "Mastercard Standard", - "card_scheme": "MASTERCARD" - } - ] -} -``` - -### Updating the Card Element with Network Schemes -```javascript -const options = { - required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. - enableCardIcon: true, // Optional, indicates whether a card icon should be enabled (only applicable for CARD_NUMBER ElementType). - enableCopy: false, // Optional, enables the copy icon to collect elements to copy text to clipboard. Defaults to 'false'). - format: String, // Optional, format for the element - translation: {}, // Optional, indicates the allowed data type value for format. - cardMetadata: {}, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). - masking: true, // Optional, indicates whether the input should be masked. Defaults to 'false'. - maskingChar: '*', // Optional, character used for masking input when masking is enabled. Defaults to '*'. -}; -``` - -`cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow accept card types based on which SDK will display card brand choice dropdown in the card number element. `Skyflow.CardType` is an enum with all skyflow supported card schemes. - -```javascript -import Skyflow from 'skyflow-js' - -const cardMetadata = { - scheme: Skyflow.CardType [] // Optional, array of skyflow supported card types. -} -``` - -- By default, SDK will populate its own auto-detected card scheme. - -### Samples - -- [Card brand choice](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/card-brand-choice.html): -This sample illustrates how to use Bin Lookup API and display the available card schemes. - -## Using Skyflow Elements to update data - -You can update the data in a vault with Skyflow Elements. Use the following steps to securely update data. - -### Step 1: Create a container -Create a container for the form elements using the `container(Skyflow.ContainerType)` method of the Skyflow client: - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) -``` - -### Step 2: Create a collect Element -Create a collect element. Collect Elements are defined as follows: - -```javascript -const collectElement = { - table: "string", // Required, the table this data belongs to. - column: "string", // Required, the column into which this data should be updated. - type: Skyflow.ElementType, // Skyflow.ElementType enum. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. - label: "string", // Optional, label for the form element. - placeholder: "string", // Optional, placeholder for the form element. - altText: "string", // (DEPRECATED) string that acts as an initial value for the collect element. - validations: [], // Optional, array of validation rules. - skyflowID: "string", // The skyflow_id of the record to be updated. -}; -const options = { - required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. - enableCardIcon: true, // Optional, indicates whether the element needs a card icon (only applicable for CARD_NUMBER ElementType). - enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). - format: String, // Optional, format for the element - translation: {}, // Optional, indicates the allowed data type value for format. -}; -const element = container.create(collectElement, options); -``` -The `table` and `column` fields indicate which table and column the Element corresponds to. - -`skyflowID` indicates the record that you want to update. - -**Notes:** -- Use dot-delimited strings to specify columns nested inside JSON fields (for example, `address.street.line1`) - -### Step 3: Mount Elements to the DOM -To specify where the Elements are rendered on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has three empty elements with unique IDs as placeholders for three Skyflow Elements. -```html -
    -
    -
    -
    -
    -
    -
    - - -``` -Now, when you call the `mount(domElement)` method, the Elements is inserted in the specified divs. For instance, the call below inserts the Element into the div with the id "#cardNumber". -```javascript -element.mount('#cardNumber'); -``` -Use the `unmount` method to reset a Collect Element to its initial state. -```javascript -element.unmount(); -``` - - -### Step 4: Update data from Elements -When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. -- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. - -```javascript -const options = { - tokens: true, // Optional, indicates whether tokens for the collected data should be returned. Defaults to 'true'. - additionalFields: { - records: [ - { - table: "string", // Table into which record should be updated. - fields: { - column1: "value", // Column names should match vault column names. - skyflowID: "value", // The skyflow_id of the record to be updated. - // ...additional fields here. - }, - }, - // ...additional records here. - ], - },// Optional - upsert: [ // Upsert operations support in the vault - { - table: "string", // Table name - column: "value", // Unique column in the table - }, - ], // Optional -}; -container.collect(options); -``` -**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. - -### End to end example of updating data with Skyflow Elements - -**Sample Code:** - -```javascript -//Step 1 -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -//Step 2 -const cardNumberElement = container.create({ - table: 'cards', - column: 'cardNumber', - inputStyles: { - base: { - color: '#1d1d1d', - }, - cardIcon: { - position: 'absolute', - left: '8px', - bottom: 'calc(50% - 12px)', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'Card Number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', -}); -const cardHolderNameElement = container.create({ - table: 'cards', - column: 'first_name', - inputStyles: { - base: { - color: '#1d1d1d', - }, - cardIcon: { - position: 'absolute', - left: '8px', - bottom: 'calc(50% - 12px)', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'Card Holder Name', - label: 'Card Holder Name', - type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', -}); - -// Step 3 -cardNumberElement.mount('#cardNumber'); // Assumes there is a div with id='#cardNumber' in the webpage. -cardHolderNameElement.mount('#cardHolderName'); // Assumes there is a div with id='#cardHolderName' in the webpage. - -// Step 4 -const nonPCIRecords = { - records: [ - { - table: 'cards', - fields: { - gender: 'MALE', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - }, - }, - ], -}; - -container.collect({ - tokens: true, - additionalFields: nonPCIRecords, -}); -``` -**Sample Response :** -```javascript -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e" - } - } - ] -} -``` - -### Validations - -Skyflow-JS provides two types of validations on Collect Elements - -#### 1. Default Validations: -Every Collect Element except of type `INPUT_FIELD` has a set of default validations listed below: -- `CARD_NUMBER`: Card number validation with checkSum algorithm(Luhn algorithm). -Available card lengths for defined card types are [12, 13, 14, 15, 16, 17, 18, 19]. -A valid 16 digit card number will be in the format - `XXXX XXXX XXXX XXXX` -- `CARD_HOLDER_NAME`: Name should be 2 or more symbols, valid characters should match pattern - `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$` -- `CVV`: Card CVV can have 3-4 digits -- `EXPIRATION_DATE`: Any date starting from current month. By default valid expiration date should be in short year format - `MM/YY` -- `PIN`: Can have 4-12 digits - -#### 2. Custom Validations: -Custom validations can be added to any element which will be checked after the default validations have passed. The following Custom validation rules are currently supported: -- `REGEX_MATCH_RULE`: You can use this rule to specify any Regular Expression to be matched with the input field value - -```javascript -const regexMatchRule = { - type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE, - params: { - regex: RegExp, - error: string // Optional, default error is 'VALIDATION FAILED'. - } -} -``` - -- `LENGTH_MATCH_RULE`: You can use this rule to set the minimum and maximum permissible length of the input field value - -```javascript -const lengthMatchRule = { - type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, - params: { - min : number, // Optional. - max : number, // Optional. - error: string // Optional, default error is 'VALIDATION FAILED'. - } -} -``` - -- `ELEMENT_VALUE_MATCH_RULE`: You can use this rule to match the value of one element with another element - -```javascript -const elementValueMatchRule = { - type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, - params: { - element: CollectElement, - error: string // Optional, default error is 'VALIDATION FAILED'. - } -} -``` - -The Sample [code snippet](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/custom-validations.html) for using custom validations: - -```javascript -/* - A simple example that illustrates custom validations. - Adding REGEX_MATCH_RULE , LENGTH_MATCH_RULE to collect element. -*/ - -// This rule allows 1 or more alphabets. -const alphabetsOnlyRegexRule = { - type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE, - params: { - regex: /^[A-Za-z]+$/, - error: 'Only alphabets are allowed', - }, -}; - -// This rule allows input length between 4 and 6 characters. -const lengthRule = { - type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, - params: { - min: 4, - max: 6, - error: 'Must be between 4 and 6 alphabets', - }, -}; - -const cardHolderNameElement = collectContainer.create({ - table: 'pii_fields', - column: 'first_name', - ...collectStylesOptions, - label: 'Card Holder Name', - placeholder: 'cardholder name', - type: Skyflow.ElementType.INPUT_FIELD, - validations: [alphabetsOnlyRegexRule, lengthRule], -}); - -/* - Reset PIN - A simple example that illustrates custom validations. - The below code shows an example of ELEMENT_VALUE_MATCH_RULE -*/ - -// For the PIN element -const pinElement = collectContainer.create({ - label: 'PIN', - placeholder: '****', - type: Skyflow.ElementType.PIN, -}); - -// This rule allows to match the value with pinElement. -const elementMatchRule = { - type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, - params: { - element: pinElement, - error: 'PIN does not match', - }, -}; - -const confirmPinElement = collectContainer.create({ - label: 'Confirm PIN', - placeholder: '****', - type: Skyflow.ElementType.PIN, - validations: [elementMatchRule], -}); - -// Mount elements on screen - errors will be shown if any of the validaitons fail. -pinElement.mount('#collectPIN'); -confirmPinElement.mount('#collectConfirmPIN'); - -``` -### Event Listener on Collect Elements - - -Helps to communicate with Skyflow elements / iframes by listening to an event - -```javascript -element.on(Skyflow.EventName,handler:function) -``` - -There are 4 events in `Skyflow.EventName` -- `CHANGE` - Change event is triggered when the Element's value changes. - -- `READY` - Ready event is triggered when the Element is fully rendered - -- `FOCUS` - Focus event is triggered when the Element gains focus - -- `BLUR` - Blur event is triggered when the Element loses focus. - -The handler ```function(state) => void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. - -```javascript -state : { - elementType: Skyflow.ElementType - isEmpty: boolean - isFocused: boolean - isValid: boolean - value: string - selectedCardScheme: Skyflow.CardType // only for CARD_NUMBER element type -} -``` - -**Note:** -- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. -- `selectedCardScheme` will exist for `CARD_NUMBER` element state and the value of Skyflow.CardType will be only populated when cardbrand choice selection is triggered otherwise, it will always be an empty string. - -##### Sample [code snippet](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/collect-element-listeners.html) for using listeners -```javascript -// Create Skyflow client. -const skyflowClient = Skyflow.init({ - vaultID: '', - vaultURL: '', - getBearerToken: () => {}, - options: { - env: Skyflow.Env.DEV, - }, -}); - -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -const cardHolderName = container.create({ - table: 'pii_fields', - column: 'first_name', - type: Skyflow.ElementType.CARDHOLDER_NAME, -}); -const cardNumber = container.create({ - table: 'pii_fields', - column: 'primary_card.card_number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -cardNumber.mount('#cardNumberContainer'); -cardHolderName.mount('#cardHolderNameContainer'); - -// Subscribing to CHANGE event, which gets triggered when element changes. -cardHolderName.on(Skyflow.EventName.CHANGE, state => { - // Your implementation when Change event occurs. - console.log(state); -}); - -// Subscribing to CHANGE event, which gets triggered when element changes. -cardNumber.on(Skyflow.EventName.CHANGE, state => { - // Your implementation when Change event occurs. - console.log(state); -}); - -``` -##### Sample Element state object when `env` is `DEV` - -```javascript -{ - elementType: 'CARDHOLDER_NAME', - isEmpty: false, - isFocused: true, - isValid: false, - value: 'John', -}; -{ - elementType: 'CARD_NUMBER', - isEmpty: false, - isFocused: true, - isValid: false, - value: '4111-1111-1111-1111', -}; -``` -##### Sample Element state object when `env` is `PROD` - -```javascript -{ - elementType: 'CARDHOLDER_NAME', - isEmpty: false, - isFocused: true, - isValid: false, - value: '', -}; -{ - elementType: 'CARD_NUMBER', - isEmpty: false, - isFocused: true, - isValid: false, - value: '4111-1111-XXXX-XXXX', -}; - -``` - -### UI Error for Collect Elements - -Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. - -`setError(error: string)` method is used to set the error text for the element, when this method is triggered, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is triggered on the same element. - -`resetError()` method is used to clear the custom error message that is set using `setError`. - -##### Sample code snippet for setError and resetError - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -const cardNumber = container.create({ - table: 'pii_fields', - column: 'primary_card.card_number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -// Set custom error. -cardNumber.setError('custom error'); - -// Reset custom error. -cardNumber.resetError(); -``` - -### Override default error Messages - -You can override the default error messages with custom ones by using `setErrorOverride`. This is especially useful to override default error messages in non-English languages. - -`setErrorOverride(message: string)` - -`setErrorOverride` overrides the default error message. When the value is invalid, the error resets automatically when the value becomes valid. - -##### Sample code snippet for setErrorOverride - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -const cardNumber = container.create({ - table: 'pii_fields', - column: 'primary_card.card_number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -// override default error. -cardHolderNameElement.on(Skyflow.EventName.BLUR, state=>{ - if(state.isEmpty) { - //can override the message when the field is required and empty - cardHolderNameElement.setErrorOverride('custom error for required'); - } else if(!state.isValid) { - //can override the message when the input is invalid - cardHolderName.setErrorOverride('custom error for invalid'); - } -}); -``` - -##### Difference between setError and setErrorOverride: - -- `setError` sets the error state on the collect element, regardless of the element's state and value (valid or invalid). Once you call `setError`, the element remains in the error state until you call `resetError`. Use `setError` to set the error state on collect element based on server-side validations. - -- `setErrorOverride` overrides the default error message. The error message resets automatically once the value becomes valid. Use `setErrorOverride` to change the default error message for a collect element. - -**Note**: -- `setErrorOverride` can only override default error messages. -- `setErrorOverride` can only be used in BLUR event listener as shown in the earlier example. - - -### Set and Clear value for Collect Elements (DEV ENV ONLY) - -`setValue(value: string)` method is used to set the value of the element. This method will override any previous value present in the element. - -`clearValue()` method is used to reset the value of the element. - -`Note:` This methods are only available in DEV env for testing/developmental purposes and MUST NOT be used in PROD env. - -##### Sample code snippet for setValue and clearValue - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -const cardNumber = container.create({ - table: 'pii_fields', - column: 'primary_card.card_number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -// Set a value programatically. -cardNumber.setValue('4111111111111111'); - -// Clear the value. -cardNumber.clearValue(); - -``` - -### Update Collect Elements - -You can update collect element properties with the `update` interface. - -The `update` interface takes the below object: - -```javascript -const updateElement = { - table: 'string', // Optional. The table this data belongs to. - column: 'string', // Optional. The column this data belongs to. - inputStyles: {}, // Optional. Styles applied to the form element. - labelStyles: {}, // Optional. Styles for the label of the element. - errorTextStyles: {}, // Optional. Styles for the errorText of element. - label: 'string', // Optional. Label for the form element. - placeholder: 'string', // Optional. Placeholder for the form element. - validations: [], // Optional. Array of validation rules. - skyflowID: 'string' // Optional. SkyflowID of the record. -}; -``` - -Only include the properties that you want to update for the specified collect element. - -Properties your provided when you created the element remain the same until you explicitly update them. - -`Note`: You can't update the `type` property of an element. - -### End to end example -```javascript -// Create a collect container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -const stylesOptions = { - inputStyles: { - base: { - fontFamily: 'Inter', - fontStyle: 'normal', - fontWeight: 400, - fontSize: '14px', - lineHeight: '21px', - width: '294px', - }, - }, - labelStyles: {}, - errorTextStyles: { - base: {}, - }, -}; - -// Create collect elements -const cardHolderNameElement = collectContainer.create({ - table: 'pii_fields', - column: 'first_name', - ...stylesOptions, - placeholder: 'Cardholder Name', - type: Skyflow.ElementType.CARDHOLDER_NAME, -}); - -const cardNumberElement = collectContainer.create({ - table: 'pii_fields', - column: 'card_number', - ...stylesOptions, - placeholder: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -const cvvElement = collectContainer.create({ - table: 'pii_fields', - column: 'cvv', - ...stylesOptions, - placeholder: 'CVV', - type: Skyflow.ElementType.CVV, -}); - -// Mount the collect elements. -cardHolderNameElement.mount('#cardHolderNameElement'); // Assumes there is a div with id='#cardHolderNameElement' in the webpage. -cardNumberElement.mount('#cardNumberElement'); // Assumes there is a div with id='#cardNumberElement' in the webpage. -cvvElement.mount('#cvvElement'); // Assumes there is a div with id='#cvvElement' in the webpage. - -// ... - -// Update validations property on cvvElement. -cvvElement.update({ - validations: [{ - type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, - params: { - max: 3, - error: 'cvv must be 3 digits', - }, - }] -}) - -// Update label, placeholder properties on cardHolderNameElement. -cardHolderNameElement.update({ - label: 'CARDHOLDER NAME', - placeholder: 'Eg: John' -}); - -// Update table, column, inputStyles properties on cardNumberElement. -cardNumberElement.update({ - table:'cards', - column:'card_number', - inputStyles:{ - base:{ - color:'blue' - } - } -}); -``` - ---- - - -## Using Skyflow File Element to upload a file - -You can upload binary files to a vault using the Skyflow File Element. Use the following steps to securely upload a file. -### Step 1: Create a container - -Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) -``` - -### Step 2: Create a File Element - -Skyflow Collect Elements are defined as follows: - -```javascript -const collectElement = { - type: Skyflow.ElementType.FILE_INPUT, // Skyflow.ElementType enum. - table: 'string', // The table this data belongs to. - column: 'string', // The column into which this data should be inserted. - skyflowID: 'string', // The skyflow_id of the record. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles:{}, // Optional, styles that will be applied to the errorText of the collect element. -} -``` -The `table` and `column` fields indicate which table and column the Element corresponds to. - -`skyflowID` indicates the record that stores the file. - -**Notes**: -- `skyflowID` is required while creating File element -- Use period-delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`). - -### Step 3: Mount elements to the DOM - -To specify where to render Elements on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has an empty div with a unique id as a placeholder for a Skyflow Element. - -```html -
    -
    -
    - - -``` - -Now, when the `mount(domElement)` method of the Element is called, the Element is inserted in the specified div. For instance, the call below inserts the Element into the div with the id "#file". - -```javascript -element.mount('#file'); -``` -Use the `unmount` method to reset a Collect Element to its initial state. - -```javascript -element.unmount(); -``` -### Step 4: Collect data from elements - -When you're ready to upload the file, call the `uploadFiles()` method on the container object. - -```javascript -container.uploadFiles(); -``` -### File upload limitations: - -- Only non-executable file are allowed to be uploaded. -- Files must have a maximum size of 32 MB -- File columns can't enable tokenization, redaction, or arrays. -- Re-uploading a file overwrites previously uploaded data. -- Partial uploads or resuming a previous upload isn't supported. - -### End-to-end file upload - -```javascript -// Step 1. -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -// Step 2. -const element = container.create({ - table: 'pii_fields', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.FILE_INPUT, -}); - -// Step 3. -element.mount('#file'); // Assumes there is a div with id='#file' in the webpage. - -// Step 4. -container.uploadFiles(); -``` - -**Sample Response :** -```javascript -{ - fileUploadResponse: [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` -### File upload with options: - -Along with fileElementInput, you can define other options in the Options object as described below: -```js -const options = { - allowedFileType: String[], // Optional, indicates the allowed file types for upload -} -``` -`allowedFileType`: An array of string value that indicates the allowedFileTypes to be uploaded. - -#### File upload with options example - -```javascript -// Create collect Container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -// Create collect elements. -const cardNumberElement = collectContainer.create({ - table: 'newTable', - column: 'card_number', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'card number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); -const options = { - allowedFileType: [".pdf",".png"]; -}; -const fileElement = collectContainer.create({ - table: 'newTable', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.FILE_INPUT, -}, - options -); - -// Mount the elements. -cardNumberElement.mount('#collectCardNumber'); -fileElement.mount('#collectFile'); - -// Collect and upload methods. -collectContainer.collect({}); -collectContainer.uploadFiles(); - -``` -**Sample Response for collect():** -```javascript -{ - "records": [ - { - "table": "newTable", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - } - } - ] -} -``` -**Sample Response for file uploadFiles() :** -```javascript -{ - "fileUploadResponse": [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` -#### File upload with additional elements - -```javascript -// Create collect Container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -// Create collect elements. -const cardNumberElement = collectContainer.create({ - table: 'newTable', - column: 'card_number', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'card number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -const fileElement = collectContainer.create({ - table: 'newTable', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.FILE_INPUT, -}); - -// Mount the elements. -cardNumberElement.mount('#collectCardNumber'); -fileElement.mount('#collectFile'); - -// Collect and upload methods. -collectContainer.collect({}); -collectContainer.uploadFiles(); - -``` -**Sample Response for collect():** -```javascript -{ - "records": [ - { - "table": "newTable", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - } - } - ] -} -``` -**Sample Response for file uploadFiles() :** -```javascript -{ - "fileUploadResponse": [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` - -Note: File name should contain only alphanumeric characters and !-_.*() - -# Securely collecting data client-side using Composable Elements -- [**Using Skyflow Composable Elements to collect data**](#using-skyflow-composable-elements-to-collect-data) -- [**Event listener on Composable Element**](#set-an-event-listener-on-composable-elements) -- [**Event listener on Composable Container**](#set-an-event-listener-on-a-composable-container) -- [**Update Composable Elements**](#update-composable-elements) -- [**Using Skyflow File Element to upload a file**](#using-skyflow-composable-file-element-to-upload-a-file) -- [**Using Skyflow File Element to upload multiple files**](#using-skyflow-composable-file-element-to-upload-multiple-files) - - -## Using Skyflow Composable Elements to collect data -Composable Elements combine multiple Skyflow Elements in a single iframe, letting you create multiple Skyflow Elements in a single row. The following steps create a composable element and securely collect data through it. - -### Step 1: Create a composable container - -Create a container for the composable element using the `container(Skyflow.ContainerType)` method of the Skyflow client: - -``` javascript - const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE,containerOptions); -``` -Pass an options object that contains the following keys: - -1. `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. - - For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. - - `Note`: The sum of values in the layout array should be equal to the number of elements created - -2. `styles`: CSS styles to apply to the composable container. -3. `errorTextStyles`: CSS styles to apply if an error is encountered. - -```javascript -const options = { - layout: [2, 1], // Required - styles: { // Optional - base: { - border: '1px solid #DFE3EB', - padding: '8px', - borderRadius: '4px', - margin: '12px 2px', - }, - }, - errorTextStyles: { // Optional - base: { - color: 'red', - fontFamily: '"Roboto", sans-serif' - }, - global: { - '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } - }, -}; -``` - -### Step 2: Create Composable Elements -Composable Elements use the following schema: - -```javascript -const composableElement = { - table: 'string', // Required. The table this data belongs to. - column: 'string', // Required. The column this data belongs to. - type: Skyflow.ElementType, // Skyflow.ElementType enum. - inputStyles: {}, // Optional. Styles applied to the form element. - labelStyles: {}, // Optional. Styles for the label of the collect element. - errorTextStyles: {}, // Optional. Styles for the errorText of the collect element. - label: 'string', // Optional. Label for the form element. - placeholder: 'string', // Optional. Placeholder for the form element. - altText: 'string', // (DEPRECATED) Initial value for the collect element. - validations: [], // Optional. Array of validation rules. -} -``` -The `table` and `column` fields indicate which table and column in the vault the Element correspond to. - -Note: Use dot-delimited strings to specify columns nested inside JSON fields (for example, `address.street.line1`). - -All elements can be styled with [JSS](https://cssinjs.org/?v=v10.7.1) syntax. - -The `inputStyles` field accepts an object of CSS properties to apply to the form element in the following states: - -* `base`: all variants inherit from these styles -* `complete`: applied when the Element has valid input -* `empty`: applied when the Element has no input -* `focus`: applied when the Element has focus -* `invalid`: applied when the Element has invalid input -* `cardIcon`: applied to the card type icon in CARD_NUMBER Element -* `copyIcon`: applied to copy icon in Elements when enableCopy option is true -* `global`: used for global styles like font-family. - -An example of an `inputStyles` object: - -```javascript -inputStyles: { - base: { - border: '1px solid #eae8ee', - padding: '10px 16px', - borderRadius: '4px', - color: '#1d1d1d', - fontFamily: '"Roboto", sans-serif' - }, - complete: { - color: '#4caf50', - }, - empty: {}, - focus: {}, - invalid: { - color: '#f44336', - }, - cardIcon: { - position: 'absolute', - left: '8px', - bottom: 'calc(50% - 12px)', - }, - copyIcon: { - position: 'absolute', - right: '8px', - }, - global: { - '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -} -``` -The states that are available for `labelStyles` are `base`, `focus`, `global`. -* requiredAsterisk: styles applied for the Asterisk symbol in the label. - -An example `labelStyles` object: - -```javascript -labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - fontFamily: '"Roboto", sans-serif' - }, - focus: { - color: '#1d1d1d' - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -} -``` - -The JS SDK supports the following composable elements: - -- `CARDHOLDER_NAME` -- `CARD_NUMBER` -- `EXPIRATION_DATE` -- `EXPIRATION_MONTH` -- `EXPIRATION_YEAR` -- `CVV` -- `INPUT_FIELD` -- `PIN` - -`Note`: Only when the entered value in the below composable elements is valid, the focus shifts automatically. The element types are: -- `CARD_NUMBER` -- `EXPIRATION_DATE` -- `EXPIRATION_MONTH` -- `EXPIRATION_YEAR` - -The `INPUT_FIELD` type is a custom UI element without any built-in validations. For information on validations, see [validations](#validations). - -Along with the Composable Element definition, you can define additional options for the element: - -```javascript -const options = { - required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false' - enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType) - format: String, // Optional, format for the element (only applicable currently for EXPIRATION_DATE ElementType), - enableCopy: false // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false') -} -``` - -- `required`: Whether or not the field is marked as required. Defaults to `false`. -- `enableCardIcon`: Whether or not the icon is visible for the CARD_NUMBER element. Defaults to `true`. -- `format`: Format pattern for the element. Only applicable to EXPIRATION_DATE and EXPIRATION_YEAR element types. -- `enableCopy`: Whether or not the copy icon is visible in collect and reveal elements. Defaults to `false`. - -The accepted `EXPIRATION_DATE` values are - -- `MM/YY` (default) -- `MM/YYYY` -- `YY/MM` -- `YYYY/MM` - - -The accepted `EXPIRATION_YEAR` values are - -- `YY` (default) -- `YYYY` - - -Once you define the Element object and options, add it to the container using the `create(element, options)` method: - -```javascript -const composableElement = { - table: 'string', // Required, the table this data belongs to. - column: 'string', // Required, the column into which this data should be inserted. - type: Skyflow.ElementType, // Skyflow.ElementType enum. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. - label: 'string', // Optional, label for the form element. - placeholder: 'string', // Optional, placeholder for the form element. - altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. - validations: [], // Optional, array of validation rules. -} - -const options = { - required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. - enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType). - format: String, // Optional, format for the element (only applicable currently for EXPIRATION_DATE ElementType). - enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). -}; - -const element = container.create(composableElement, options); -``` - -### Step 3: Mount Container to the DOM -To specify where the Elements are rendered on your page, create a placeholder `
    ` element with unique `id` attribute. Use this empty `
    ` placeholder to mount the composable container. - -```javascript -
    -
    -
    -
    - - -``` -Use the composable container's `mount(domElement)` method to insert the container's Elements into the specified `
    `. For instance, the following call inserts Elements into the `
    ` with the `id "#composableContainer"`. - -```javacript -container.mount('#composableContainer'); -``` - -### Step 4: Collect data from elements - - -When the form is ready to be submitted, call the container's `collect(options?)` method. The options parameter takes an object of optional parameters as follows: -- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. - -```javascript -const options = { - tokens: true, // Optional, indicates whether tokens for the collected data should be returned. Defaults to 'true'. - additionalFields: { - records: [ - { - table: 'string', // Table into which record should be inserted. - fields: { - column1: 'value', // Column names should match vault column names. - // ...additional fields here. - }, - }, - // ...additional records here. - ], - }, // Optional - upsert: [ // Upsert operations support in the vault - { - table: 'string', // Table name - column: 'value', // Unique column in the table - }, - ], // Optional -}; -``` - -### End to end example of collecting data with Composable Elements - -```javascript -// Step 1 -const containerOptions = { - layout: [2, 1], - styles: { - base: { - border: '1px solid #eae8ee', - padding: '10px 16px', - borderRadius: '4px', - margin: '12px 2px', - }, - }, - errorTextStyles: { - base: { - color: 'red', - }, - }, -}; - -const composableContainer = skyflowClient.container( - Skyflow.ContainerType.COMPOSABLE, - containerOptions -); - -// Step 2 - -const collectStylesOptions = { - inputStyles: { - base: { - fontFamily: 'Inter', - fontStyle: 'normal', - fontWeight: 400, - fontSize: '14px', - lineHeight: '21px', - width: '294px', - }, - }, - labelStyles: {}, - errorTextStyles: { - base: {}, - }, -}; - -const cardHolderNameElement = composableContainer.create({ - table: 'pii_fields', - column: 'first_name', - ...collectStylesOptions, - placeholder: 'Cardholder Name', - type: Skyflow.ElementType.CARDHOLDER_NAME, -}); - -const cardNumberElement = composableContainer.create({ - table: 'pii_fields', - column: 'card_number', - ...collectStylesOptions, - placeholder: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -const cvvElement = composableContainer.create({ - table: 'pii_fields', - column: 'cvv', - ...collectStylesOptions, - placeholder: 'CVV', - type: Skyflow.ElementType.CVV, -}); - -// Step 3 -composableContainer.mount('#composableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. - -// Step 4 -composableContainer.collect({ - tokens: true, -}); -``` -### Sample Response: - -```javascript -{ - "records": [ - { - "table": "pii_fields", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "first_name": "63b5eeee-3624-493f-825e-137a9336f882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "7baf5bda-aa22-4587-a5c5-412f6f783a19", - } - } - ] -} -``` -For information on validations, see [validations](#validations). - -### Set an event listener on Composable Elements: - -You can communicate with Skyflow Elements by listening to element events: - -```javascript -element.on(Skyflow.EventName,handler:function) -``` - - -The SDK supports four events: - -- `CHANGE`: Triggered when the Element's value changes. -- `READY`: Triggered when the Element is fully rendered. -- `FOCUS`: Triggered when the Element gains focus. -- `BLUR`: Triggered when the Element loses focus. - -The handler `function(state) => void` is a callback function you provide that's called when the event is fired with a state object that uses the following schema: - -```javascript -state : { - elementType: Skyflow.ElementType - isEmpty: boolean - isFocused: boolean - isValid: boolean - value: string -} -``` -`Note`: Events only include element values when in the state object when env is DEV. By default, value is an empty string. - -### Example Usage of Event Listener on Composable Elements - -```javascript -const containerOptions = { - layout: [1], - styles: { - base: { - border: '1px solid #eae8ee', - padding: '10px 16px', - borderRadius: '4px', - margin: '12px 2px', - } - }, - errorTextStyles: { - base: { - color: 'red' - } - } -} - -const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -const cvv = composableContainer.create({ - table: 'pii_fields', - column: 'primary_card.cvv', - type: Skyflow.ElementType.CVV, -}); - -composableContainer.mount('#cvvContainer'); - -// Subscribing to CHANGE event, which gets triggered when element changes. -cvv.on(Skyflow.EventName.CHANGE, state => { -// Your implementation when Change event occurs. -console.log(state); -}); -``` - -Sample Element state object when env is `DEV` - -```javascript -{ - elementType: 'CVV' - isEmpty: false - isFocused: true - isValid: false - value: '411' -} -``` - -Sample Element state object when env is `PROD` - -```javascript -{ - elementType: 'CVV' - isEmpty: false - isFocused: true - isValid: false - value: '' -} -``` - -### Update composable elements -You can update composable element properties with the `update` interface. - - -The `update` interface takes the below object: -```javascript -const updateElement = { - table: 'string', // Optional. The table this data belongs to. - column: 'string', // Optional. The column this data belongs to. - inputStyles: {}, // Optional. Styles applied to the form element. - labelStyles: {}, // Optional. Styles for the label of the element. - errorTextStyles: {}, // Optional. Styles for the errorText of element. - label: 'string', // Optional. Label for the form element. - placeholder: 'string', // Optional. Placeholder for the form element. - validations: [], // Optional. Array of validation rules. -}; -``` - -Only include the properties that you want to update for the specified composable element. - -Properties your provided when you created the element remain the same until you explicitly update them. - -`Note`: You can't update the `type` property of an element. - -### End to end example -```javascript -const containerOptions = { layout: [2, 1] }; - -// Create a composable container. -const composableContainer = skyflowClient.container( - Skyflow.ContainerType.COMPOSABLE, - containerOptions -); - -const stylesOptions = { - inputStyles: { - base: { - fontFamily: 'Inter', - fontStyle: 'normal', - fontWeight: 400, - fontSize: '14px', - lineHeight: '21px', - width: '294px', - }, - }, - labelStyles: {}, - errorTextStyles: { - base: {}, - }, -}; - -// Create composable elements. -const cardHolderNameElement = composableContainer.create({ - table: 'pii_fields', - column: 'first_name', - ...stylesOptions, - placeholder: 'Cardholder Name', - type: Skyflow.ElementType.CARDHOLDER_NAME, -}); - - -const cardNumberElement = composableContainer.create({ - table: 'pii_fields', - column: 'card_number', - ...stylesOptions, - placeholder: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -const cvvElement = composableContainer.create({ - table: 'pii_fields', - column: 'cvv', - ...stylesOptions, - placeholder: 'CVV', - type: Skyflow.ElementType.CVV, -}); - -// Mount the composable container. -composableContainer.mount('#compostableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. - -// ... - -// Update validations property on cvvElement. -cvvElement.update({ - validations: [{ - type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, - params: { - max: 3, - error: 'cvv must be 3 digits', - }, - }] -}) - -// Update label, placeholder properties on cardHolderNameElement. -cardHolderNameElement.update({ - label: 'CARDHOLDER NAME', - placeholder: 'Eg: John' -}); - -// Update table, column, inputStyles properties on cardNumberElement. -cardNumberElement.update({ - table:'cards', - column:'card_number', - inputStyles:{ - base:{ - color:'blue' - } - } -}); - - -``` -### Set an event listener on a composable container -Currently, the SDK supports one event: -- `SUBMIT`: Triggered when the `Enter` key is pressed in any container element. - -The handler `function(void) => void` is a callback function you provide that's called when the `SUBMIT' event fires. - -### Example -```javascript -const containerOptions = { layout: [1] } - -// Creating a composable container. -const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Creating the element. -const cvv = composableContainer.create({ - table: 'pii_fields', - column: 'primary_card.cvv', - type: Skyflow.ElementType.CVV, -}); - -// Mounting the container. -composableContainer.mount('#cvvContainer'); - -// Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. -composableContainer.on(Skyflow.EventName.SUBMIT, ()=> { - // Your implementation when the SUBMIT(enter) event occurs. - console.log('Submit Event Listener is being Triggered.'); -}); -``` - -## Using Skyflow Composable File Element to upload a file -You can upload binary files to a vault using the Skyflow File Element. Use the following steps to securely upload a file. -### Step 1: Create a container - -Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: - -```javascript -const containerOptions = { layout: [1] } - -// Creating a composable container. -const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); -``` - -### Step 2: Create a File Element - -Skyflow Collect Elements are defined as follows: - -```javascript -const collectElement = { - type: Skyflow.ElementType.FILE_INPUT, // Skyflow.ElementType enum. - table: 'string', // The table this data belongs to. - column: 'string', // The column into which this data should be inserted. - skyflowID: 'string', // The skyflow_id of the record. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles:{}, // Optional, styles that will be applied to the errorText of the collect element. -} -``` -The `table` and `column` fields indicate which table and column the Element corresponds to. - -`skyflowID` indicates the record that stores the file. - -**Notes**: -- `skyflowID` is required while creating File element -- Use period-delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`). - -### Step 3: Mount Container to the DOM -Mount Elements for file upload to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-container-to-the-dom). - -### Step 4: Collect data from elements - -When you're ready to upload the file, call the `uploadFiles()` method on the container object. - -```javascript -composableContainer.uploadFiles(); -``` -### File upload limitations: - -- Only non-executable file are allowed to be uploaded. -- Files must have a maximum size of 32 MB -- File columns can't enable tokenization, redaction, or arrays. -- Re-uploading a file overwrites previously uploaded data. -- Partial uploads or resuming a previous upload isn't supported. - -### End-to-end file upload - -```javascript -// Step 1. -const containerOptions = { layout: [1] } - -// Creating a composable container. -const container = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Step 2. -const element = container.create({ - table: 'pii_fields', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.FILE_INPUT, -}); - -// Step 3. -container.mount('#file'); // Assumes there is a div with id='#file' in the webpage. - -// Step 4. -container.uploadFiles(); -``` - -**Sample Response :** -```javascript -{ - fileUploadResponse: [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` -### File upload with options: - -Along with fileElementInput, you can define other options in the Options object as described below: -```js -const options = { - allowedFileType: String[], // Optional, indicates the allowed file types for upload -} -``` -`allowedFileType`: An array of string value that indicates the allowedFileTypes to be uploaded. - -#### File upload with options example - -```javascript -// Create collect Container. -const containerOptions = { layout: [1] } - -// Creating a composable container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Create collect elements. -const cardNumberElement = collectContainer.create({ - table: 'newTable', - column: 'card_number', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'card number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); -const options = { - allowedFileType: [".pdf",".png"]; -}; -const fileElement = collectContainer.create({ - table: 'newTable', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.FILE_INPUT, -}, - options -); - -// Mount the elements. -collectContainer.mount('#collectContainer'); - -// Collect and upload methods. -collectContainer.collect({}); -collectContainer.uploadFiles(); - -``` -**Sample Response for collect():** -```javascript -{ - "records": [ - { - "table": "newTable", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - } - } - ] -} -``` -**Sample Response for file uploadFiles() :** -```javascript -{ - "fileUploadResponse": [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` -#### File upload with additional elements - -```javascript -// Create collect Container. -const containerOptions = { layout: [1,1] } - -// Creating a composable container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Create collect elements. -const cardNumberElement = collectContainer.create({ - table: 'newTable', - column: 'card_number', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'card number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -const fileElement = collectContainer.create({ - table: 'newTable', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.FILE_INPUT, -}); - -// Mount the elements. -cardNumberElement.mount('#collectCardNumber'); -fileElement.mount('#collectFile'); - -// Collect and upload methods. -collectContainer.collect({}); -collectContainer.uploadFiles(); - -``` -**Sample Response for collect():** -```javascript -{ - "records": [ - { - "table": "newTable", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - } - } - ] -} -``` -**Sample Response for file uploadFiles() :** -```javascript -{ - "fileUploadResponse": [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` - -Note: File name should contain only alphanumeric characters and !-_.*() - - -## Using Skyflow Composable File Element to upload multiple files -You can upload binary files to a vault using the Skyflow File Element. Use the following steps to securely upload a file. -### Step 1: Create a container - -Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: - -```javascript -const containerOptions = { layout: [1] } - -// Creating a composable container. -const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); -``` - -### Step 2: Create a File Element - -Skyflow Collect Elements are defined as follows: - -```javascript -const collectElement = { - type: Skyflow.ElementType.MULTI_FILE_INPUT, // Skyflow.ElementType enum. - table: 'string', // The table this data belongs to. - column: 'string', // The column into which this data should be inserted. - inputStyles: {}, // Optional, styles that should be applied to the form element. - labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. - errorTextStyles:{}, // Optional, styles that will be applied to the errorText of the collect element. -} -``` -The `table` and `column` fields indicate which table and column the Element corresponds to. - -**Notes**: -- Use period-delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`). - -### Step 3: Mount container to the DOM -Elements used for rendering files are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-dom-1). - -### Step 4: Collect data from elements - -When you're ready to upload the file, call the `uploadMultipleFiles()` method on the element. - -```javascript -const metaData = {card_number: '123'} // Optional: used to generate Skyflow IDs, and upload files to those IDs - -element.uploadMultipleFiles(); -``` -Note: -- If `MetaData` is provided, that will be used to generate Skyflow IDs, and upload files to those IDs -- If `MetaData` is not provided, the files will be uploaded as a new record. - -### File upload limitations: - -- Only non-executable file are allowed to be uploaded. -- Files have a default maximum size of 32 MB per file. This limit is configurable using the `maxFileSize` option. -- Up to 4 files can be uploaded at a time by default. This limit is configurable using the `maxFileCount` option. -- File columns can't enable tokenization, redaction, or arrays. -- Re-uploading a file overwrites previously uploaded data. -- Partial uploads or resuming a previous upload isn't supported. - -### End-to-end file upload - -```javascript -// Step 1. -const containerOptions = { layout: [1] } - -// Creating a composable container. -const container = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Step 2. -const element = container.create({ - table: 'pii_fields', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.MULTI_FILE_INPUT, -}); - -// Step 3. -container.mount('#file'); // Assumes there is a div with id='#file' in the webpage. - -// Step 4. -element.uploadMultipleFiles(); -``` - -**Sample Response :** -```javascript -{ - fileUploadResponse: [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` -### File upload with options: - -Along with fileElementInput, you can define other options in the Options object as described below: -```js -const options = { - allowedFileType: String[], // Optional. Restricts uploads to the listed file extensions (e.g. [".pdf", ".png"]). - blockEmptyFiles: Boolean, // Optional. When true, rejects files with 0 bytes. Default: false. - preserveFileName: Boolean, // Optional. When true, keeps the original filename on upload. Default: false. - maxFileSize: Number, // Optional. Maximum size in bytes for each individual file. Default: 32000000 (32 MB). - maxFileCount: Number, // Optional. Maximum number of files that can be selected at once. Must be a positive integer. Default: 4. -} -``` - -- `allowedFileType`: An array of strings indicating which file extensions are accepted for upload. -- `blockEmptyFiles`: When `true`, files with a size of 0 bytes are rejected. -- `preserveFileName`: When `true`, the original filename is preserved on upload. -- `maxFileSize`: Maximum allowed size **per file**, in bytes. If any file exceeds this limit, a validation error is shown with the filename. Defaults to `32000000` (32 MB). Only applies to `MULTI_FILE_INPUT` elements. -- `maxFileCount`: Maximum number of files that can be selected for a single upload. Must be a positive integer. Defaults to `4`. Only applies to `MULTI_FILE_INPUT` elements. - -#### File upload with options example - -```javascript -// Create collect Container. -const containerOptions = { layout: [1] } - -// Creating a composable container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Create collect elements. -const cardNumberElement = collectContainer.create({ - table: 'newTable', - column: 'card_number', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'card number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); -const options = { - allowedFileType: [".pdf", ".png"], - maxFileSize: 5000000, // 5 MB per file - maxFileCount: 3, // up to 3 files at once -}; -const fileElement = collectContainer.create({ - table: 'newTable', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.MULTI_FILE_INPUT, -}, - options -); - -// Mount the elements. -collectContainer.mount('#collectContainer'); - -// Collect and upload methods. -collectContainer.collect({}); -fileElement.uploadMultipleFiles(); - -``` -**Sample Response for collect():** -```javascript -{ - "records": [ - { - "table": "newTable", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - } - } - ] -} -``` -**Sample Response for file uploadFiles() :** -```javascript -{ - "fileUploadResponse": [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - } - ] -} -``` -#### File upload with additional elements - -```javascript -// Create collect Container. -const containerOptions = { layout: [1,1] } - -// Creating a composable container. -const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); - -// Create collect elements. -const cardNumberElement = collectContainer.create({ - table: 'newTable', - column: 'card_number', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - placeholder: 'card number', - label: 'Card Number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -const fileElement = collectContainer.create({ - table: 'newTable', - column: 'file', - skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', - inputstyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - type: Skyflow.ElementType.MULTI_FILE_INPUT, -}); - -// Mount the elements. -collectContainer.mount('#collectContainer'); - -// Collect and upload methods. -collectContainer.collect({}); -fileElement.uploadMultipleFiles(); - -``` -**Sample Response for collect():** -```javascript -{ - "records": [ - { - "table": "newTable", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - } - } - ] -} -``` -**Sample Response for file uploadFiles() :** -```javascript -{ - "fileUploadResponse": [ - { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" - }, - { - "skyflow_id": "546eaa6c-5c15-4513-aa15-29f50babe809" - } - ] -} -``` -Note: File name should contain only alphanumeric characters and !-_.*() - ---- - - -# Securely revealing data client-side -- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) -- [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) -- [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) -- [**Set token for Reveal Elements**](#set-token-for-reveal-elements) -- [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) -- [**Render a file with a File Element**](#render-a-file-with-a-file-element) -- [**Update Reveal Elements**](#update-reveal-elements) -- [**Using Composable Reveal Elements to reveal data**](#using-composable-reveal-elements-to-reveal-data) -- [**Update Composable Reveal Elements**](#update-reveal-composable-elements) -- [**Render a file with a composable file element**](#render-a-file-with-a-composable-file-element) - - -## Retrieving data from the vault - -For non-PCI use-cases, retrieving data from the vault and revealing it in the browser can be done either using the SkyflowID's, unique column values or tokens as described below - -- ### Using Skyflow tokens - In order to retrieve data from your vault using tokens that you have previously generated for that data, you can use the `detokenize(records)` method. The records parameter takes a JSON object that contains `records` to be fetched as shown below. - -```javascript -const records = { - records: [ - { - token: 'string', // Token for the record to be fetched. - redaction: RedactionType // Optional. Redaction to be applied for retrieved data. - }, - ], -}; - -Note: If you do not provide a redaction type, RedactionType.PLAIN_TEXT is the default. - -skyflow.detokenize(records); -``` -An [example](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/pure-js.html) of a detokenize call: - -```javascript -skyflow.detokenize({ - records: [ - { - token: '131e70dc-6f76-4319-bdd3-96281e051051', - }, - { - token: '1r434532-6f76-4319-bdd3-96281e051051', - redaction: Skyflow.RedactionType.MASKED - } - ], -}); -``` - -The sample response: -```javascript -{ - "records": [ - { - "token": "131e70dc-6f76-4319-bdd3-96281e051051", - "value": "1990-01-01", - "valueType": "STRING" - }, - { - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "value": "xxxxxxer", - "valueType": "STRING" - } - ] -} -``` - -- ### Using Skyflow ID's or Unique Column Values - You can retrieve data from the vault with the `get(records, options)` method using either Skyflow IDs or unique column values. - - The records parameter accepts a JSON object that contains an array of either Skyflow IDs or unique column names and values. - - The options is an optional `IGetOptions` object that retrieves the tokens for SkyflowIDs. - - Notes: - - - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. - - `options` parameter is applicable only for retrieving tokens using Skyflow ID. - - You can't pass options along with the redaction type. - - `tokens` defaults to false. - - Skyflow.RedactionTypes accepts four values: - - `PLAIN_TEXT` - - `MASKED` - - `REDACTED` - - `DEFAULT` - - You must apply a redaction type to retrieve data. - -#### Schema (Skyflow IDs) - -```javascript -data = { - records: [ - { - ids: ["SKYFLOW_ID_1", "SKYFLOW_ID_2"], // List of skyflow_ids for the records to fetch. - table: "NAME_OF_SKYFLOW_TABLE", // Name of table holding the records in the vault. - redaction: Skyflow.RedactionType, // Redaction type to apply to retrieved data. - }, - ], -}; -``` -#### Schema (Unique column values) - -```javascript -data = { - records: [ - { - table: "NAME_OF_SKYFLOW_TABLE", // Name of table holding the records in the vault. - columnName: "UNIQUE_COLUMN_NAME", // Unique column name in the vault. - columnValues: [ // List of given unique column values. - "", - "", - ], // Required when specifying a unique column - redaction: Skyflow.RedactionType, // Redaction type applies to retrieved data. - - }, - ], -}; -``` -[Example usage (Skyflow IDs)](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/get-pure-js.html) - -```javascript -skyflow.get({ - records: [ - { - ids: ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9"], - table: "cards", - redaction: Skyflow.RedactionType.PLAIN_TEXT, - }, - { - ids: ["da26de53-95d5-4bdb-99db-8d8c66a35ff9"], - table: "contacts", - redaction: Skyflow.RedactionType.PLAIN_TEXT, - }, - ], -}); -``` -Example response - -```javascript -{ - "records": [ - { - "fields": { - "card_number": "4111111111111111", - "cvv": "127", - "expiry_date": "11/2035", - "fullname": "myname", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["da26de53-95d5-4bdb-99db-8d8c66a35ff9"] - } - ] -} -``` -[Example usage (Unique column values)](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/get-pure-js.html) - -```javascript -skyflow.get({ - records: [ - { - table: "cards", - redaction: RedactionType.PLAIN_TEXT, - columnName: "card_id", - columnValues: ["123", "456"], - } - ], -}); -``` -Sample response: -```javascript -{ - "records": [ - { - "fields": { - "card_id": "123", - "expiry_date": "11/35", - "fullname": "myname", - "id": "f8d2-b557-4c6b-a12c-c5ebfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_id": "456", - "expiry_date": "10/23", - "fullname": "sam", - "id": "da53-95d5-4bdb-99db-8d8c5ff9" - }, - "table": "cards" - } - ] -} -``` - -[Example usage (Fetch tokens using Skyflow IDs)](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/get-pure-js.html) -```javascript -skyflow.get({ - records: [ - { - ids: [ - "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9", - "da26de53-95d5-4bdb-99db-8d8c66a35ff9" - ], - table: "cards", - }, - ], -}, { tokens: true }); -``` -Sample response: -```javascript -{ - "records": [ - { - "fields": { - "card_id": "f689e421-4cf8-4438-8dbd-cc8e7654b7d9", - "expiry_date": "d9ef1cb8-5c22-48b0-b769-64ac20ccee01", - "fullname": "37480f82-d237-4efc-a06a-ebe57121be06", - "id": "f8d2-b557-4c6b-a12c-c5ebfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_id": "d794b64c-e283-4fb8-8eef-9f6710730b69", - "expiry_date": "ff848fc3-a093-4ed4-9414-877b74a33111", - "fullname": "dfb6c247-3ee6-4fd2-8d1e-19d8e11c25ce", - "id": "da53-95d5-4bdb-99db-8d8c5ff9" - }, - "table": "cards" - } - ] -} -``` - -## Using Skyflow Elements to reveal data - -Skyflow Elements can be used to securely reveal data in a browser without exposing your front end to the sensitive data. This is great for use cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. - -### Step 1: Create a container -To start, create a container using the `container(Skyflow.ContainerType)` method of the Skyflow client as shown below. - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL) -``` - -### Step 2: Create a reveal Element - -Then define a Skyflow Element to reveal data as shown below. - -```javascript -const revealElement = { - token: 'string', // Required, token of the data being revealed. - inputStyles: {}, // Optional, styles to be applied to the element. - labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. - label: 'string', // Optional, label for the form element. - altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. - redaction: RedactionType, //Optional, Redaction Type to be applied to data, RedactionType.PLAIN_TEXT will be applied if not provided. -}; -``` - -Note: If you don't provide a redaction type, RedactionType.PLAIN_TEXT will apply by default. - -The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data. But for reveal element, `inputStyles` accepts only `base` variant, `copyIcon` and `global` style objects. - -An example of a inputStyles object: - -```javascript -inputStyles: { - base: { - color: '#1d1d1d', - }, - copyIcon: { - position: 'absolute', - right: '8px', - top: 'calc(50% - 10px)', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -An example of a labelStyles object: - -```javascript -labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -An example of a errorTextStyles object: - -```javascript -errorTextStyles: { - base: { - color: '#f44336', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -Along with RevealElementInput, you can define other options in the RevealElementOptions object as described below: -```js -const options = { - enableCopy: false, // Optional, enables the copy icon to reveal elements to copy text to clipboard. Defaults to 'false'). - format: String, // Optional, format for the element - translation: {} // Optional, indicates the allowed data type value for format. -} -``` - -`format`: A string value that indicates how the reveal element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified to any character in the `format` value is considered as a string literal. - -`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `{ ‘X’: ‘[0-9]’ }`. - -**Reveal Element Options examples:** -Example 1 -```js -const revealElementInput = { - token: '' -}; - -const options = { - format: '(XXX) XXX-XXXX', - translation: { 'X': '[0-9]'} -}; - -const revealElement = revealContainer.create(revealElementInput,options); -``` - -Value from vault: "1234121234" -Revealed Value displayed in element: "(123) 412-1234" - -Example 2: -```js -const revealElementInput = { - token: '' -}; - -const options = { - format: 'XXXX-XXXXXX-XXXXX', - translation: { 'X': '[0-9]' } -}; - -const revealElement = revealContainer.create(revealElementInput,options); -``` - -Value from vault: "374200000000004" -Revealed Value displayed in element: "3742-000000-00004" - -Once you've defined a Skyflow Element, you can use the `create(element)` method of the container to create the Element as shown below: - -```javascript -const element = container.create(revealElement) -``` - -### Step 3: Mount Elements to the DOM - -Elements used for revealing data are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-dom). - - -### Step 4: Reveal data -When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: - -```javascript -container - .reveal() - .then(data => { - // Handle success. - }) - .catch(err => { - // Handle error. - }); -``` - - -### End to end example of all steps - -**[Sample Code:](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/skyflow-elements.html)** -```javascript -// Step 1. -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -// Step 2. -const cardNumberElement = container.create({ - token: 'b63ec4e0-bbad-4e43-96e6-6bd50f483f75', - inputStyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - label: 'card_number', - altText: 'XXXX XXXX XXXX XXXX', - redaction: Skyflow.RedactionType.MASKED -}); - -const cvvElement = container.create({ - token: '89024714-6a26-4256-b9d4-55ad69aa4047', - inputStyles: { - base: { - color: '#1d1d1d', - }, - }, - label: 'cvv', - altText: 'XXX', -}); - -const expiryDate= container.create({ - token: 'a4b24714-6a26-4256-b9d4-55ad69aa4047', - inputStyles: { - base: { - color: '#1d1d1d', - }, - }, - label: 'expiryDate', - altText: 'MM/YYYY', -}); -// Step 3. -cardNumberElement.mount('#cardNumber'); // Assumes there is a placeholder div with id='cardNumber' on the page -cvvElement.mount('#cvv'); // Assumes there is a placeholder div with id='cvv' on the page -expiryDate.mount('#expiryDate'); // Assumes there is a placeholder div with id='expiryDate' on the page - -// Step 4. -container - .reveal() - .then(data => { - // Handle success. - }) - .catch(err => { - // Handle error. - }); -``` - -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. - -### Sample Response - -``` -{ - "success": [ - { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - "value": "xxxxxxxxx4163" - "valueType": "STRING" - }, - { - "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", - "value": "12/2098" - "valueType": "STRING" - } - ], - "errors": [ - { - "token": "89024714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047" - } - } - ] -} -``` - -### UI Error for Reveal Elements -Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. - -`setError(error: string)` method is used to set the error text for the element, when this method is triggered, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is triggered on the same element. - -`resetError()` method is used to clear the custom error message that is set using `setError`. - -##### Sample code snippet for setError and resetError - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -const cardNumber = container.create({ - token: '89024714-6a26-4256-b9d4-55ad69aa4047', -}); - -// Set custom error. -cardNumber.setError('custom error'); - -// Reset custom error. -cardNumber.resetError(); -``` - -### Override default error messages - -You can override the default error messages with custom ones by using `setErrorOverride`. This is especially useful to override default error messages in non-English languages. - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -const cardNumber = container.create({ - token: '89024714-6a26-4256-b9d4-55ad69aa4047', -}); - -const revealButton = document.getElementById('revealPCIData'); - -if (revealButton) { - revealButton.addEventListener('click', () => { - revealContainer.reveal().then((res) => { - //handle reveal response - }).catch((err) => { - cardNumber.setErrorOverride("custom error") - }); - }); -} -``` - -### Set token for Reveal Elements - -The `setToken(value: string)` method can be used to set the token of the Reveal Element. If no altText is set, the set token will be displayed on the UI as well. If altText is set, then there will be no change in the UI but the token of the element will be internally updated. - -##### Sample code snippet for setToken -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -const cardNumber = container.create({ - altText: 'Card Number', -}); - -// Set token. -cardNumber.setToken('89024714-6a26-4256-b9d4-55ad69aa4047'); -``` -### Set and Clear altText for Reveal Elements -The `setAltText(value: string)` method can be used to set the altText of the Reveal Element. This will cause the altText to be displayed in the UI regardless of whether the token or value is currently being displayed. - -`clearAltText()` method can be used to clear the altText, this will cause the element to display the token or actual value of the element. If the element has no token, the element will be empty. -##### Sample code snippet for setAltText and clearAltText - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -const cardNumber = container.create({ - token: '89024714-6a26-4256-b9d4-55ad69aa4047', -}); - -// Set altText. -cardNumber.setAltText('Card Number'); - -// Clear altText. -cardNumber.clearAltText(); - -``` - -## Render a file with a File Element - -You can render files using the Skyflow File Element. Use the following steps to securely render a file. - -### Step 1: Create a container -Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL) -``` - -### Step 2: Create a File Element -Define a Skyflow Element to render the file as shown below. - -```javascript -const fileElement = { - inputStyles: {}, // Optional, styles to be applied to the element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the render element. - altText: 'string', // Optional, string that is shown before file render call - skyflowID: 'string', // Required, skyflow id of the file to render - column: 'string', // Required, column name of the file to render - table: 'string', // Required, table name of the file to render -}; -``` -The inputStyles and errorTextStyles parameters accept a styles object as described in the [previous section](https://github.com/skyflowapi/skyflow-js#step-2-create-a-collect-element) for collecting data. But for render file elements, inputStyles accepts only base variant, global style objects. - -An example of a inputStyles object: - -```javascript -inputStyles: { - base: { - height: '400px', - width: '300px', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -} -``` -An example of a errorTextStyles object: -```javascript -errorTextStyles: { - base: { - color: '#f44336', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -} -``` - -### Step 3: Mount Elements to the DOM -Elements used for rendering files are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](https://github.com/skyflowapi/skyflow-js#step-3-mount-elements-to-the-dom). - -### Step 4: Render File -After you create and mount the element, call the `renderFile()` method on the element as shown below: -```javascript -fileElement - .renderFile() - .then(data => { - // Handle success. - }) - .catch(err => { - // Handle error. - }); -``` - -### End to end example of file render -```javascript -// Step 1. -const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -// REPLACE with your custom implementation to fetch skyflow_id from backend service. -// Sample implementation -fetch("") - .then((response) => { - - // on successful fetch skyflow_id - const skyflowID = response.skyflow_id; - - // Step 2. - const fileElement = container.create({ - skyflowID: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - column: "file", - table: "table", - inputStyles: { - base: { - height: "400px", - width: "300px", - }, - }, - errorTextStyles: { - base: { - color: "#f44336", - }, - }, - altText: "This is an altText", - }); - // Step 3. - fileElement.mount("#renderFile"); // Assumes there is a placeholder div with id=renderFile on the page - - const renderButton = document.getElementById("renderFiles"); // button to call render file - - if (renderButton) { - renderButton.addEventListener("click", () => { - - // Step 4. - fileElement - .renderFile() - .then((data) => { - // Handle success. - }) - .catch((err) => { - // Handle error. - }); - }); - } - }) - .catch((err) => { - // failed to fetch skyflow_id - console.log(err); - }); - -``` - -### Sample Success Response -```json -{ - "success": [ - { - "skyflow_id": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - "column": "file" - }, - ] -} -``` - -## Update Reveal Elements - -You can update reveal element properties with the `update` interface. - -The `update` interface takes the below object: -```javascript -const updateElement = { - token: 'string', // Optional, token of the data being revealed. - inputStyles: {}, // Optional, styles to be applied to the element. - labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. - label: 'string', // Optional, label for the form element. - altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. - redaction: RedactionType, // Optional, Redaction Type to be applied to data. - skyflowID: 'string', // Optional, Skyflow ID of the file to render. - table: 'string', // Optional, table name of the file to render. - column: 'string' // Optional, column name of the file to render. -}; -``` - -Only include the properties that you want to update for the specified reveal element. - -Properties your provided when you created the element remain the same until you explicitly update them. - -### End to end example -```javascript -// Create a reveal container. -const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); - -const stylesOptions = { - inputStyles: { - base: { - fontFamily: 'Inter', - fontStyle: 'normal', - fontWeight: 400, - fontSize: '14px', - lineHeight: '21px', - width: '294px', - }, - }, - labelStyles: {}, - errorTextStyles: { - base: { - color: '#f44336' - }, - }, -}; - -// Create reveal elements -const cardHolderNameRevealElement = revealContainer.create({ - token: 'ed5fdd1f-5009-435c-a06b-3417ce76d2c8', - altText: 'first name', - ...stylesOptions, - label: 'Card Holder Name', -}); - -const cardNumberRevealElement = revealContainer.create({ - token: '8ee84061-7107-4faf-bb25-e044f3d191fe', - altText: 'xxxx', - ...stylesOptions, - label: 'Card Number', - redaction: 'RedactionType.CARD_NUMBER' -}); - -// Mount the reveal elements. -cardHolderNameRevealElement.mount('#cardHolderNameRevealElement'); // Assumes there is a div with id='#cardHolderNameRevealElement' in the webpage. -cardNumberRevealElement.mount('#cardNumberRevealElement'); // Assumes there is a div with id='#cardNumberRevealElement' in the webpage. - -// ... - -// Update label, labelStyles properties on cardHolderNameRevealElement. -cardHolderNameRevealElement.update({ - label: 'CARDHOLDER NAME', - labelStyles: { - base: { - color: '#aa11aa' - } - } -}); - -// Update inputStyles, errorTextStyles properties on cardNumberRevealElement. -cardNumberRevealElement.update({ - inputStyles: { - base: { - color: '#fff', - backgroundColor: '#000', - borderColor: '#f00', - borderWidth: '5px' - } - }, - errorTextStyles: { - base: { - backgroundColor: '#000', - } - } -}); -``` - ---- - - -## Using Composable Reveal Elements to reveal data - -Composable Reveal Elements combine multiple Skyflow Elements in a single iframe, letting you create multiple Skyflow Elements in a single row. The following steps create a composable reveal element and securely collect data through it. - -### Step 1: Create a composable reveal container - -Create a container for the composable reveal element using the `container(Skyflow.ContainerType)` method of the Skyflow client: - -``` javascript - const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); -``` -Pass an options object that contains the following keys: - -1. `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. - - For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. - - `Note`: The sum of values in the layout array should be equal to the number of elements created - -2. `styles`: CSS styles to apply to the reveal composable container. -3. `errorTextStyles`: CSS styles to apply if an error is encountered. - -```javascript -const containerOptions = { - layout: [2, 1], // Required - styles: { // Optional - base: { - border: '1px solid #DFE3EB', - padding: '8px', - borderRadius: '4px', - margin: '12px 2px', - }, - }, - errorTextStyles: { // Optional - base: { - color: 'red', - fontFamily: '"Roboto", sans-serif' - }, - global: { - '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } - }, -}; -``` - -### Step 2: Create Composable Reveal Elements -Composable Reveal Elements use the following schema: - -```javascript -const revealComposableElement = { - token: 'string', // Required, token of the data being revealed. - inputStyles: {}, // Optional, styles to be applied to the element. - labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. - label: 'string', // Optional, label for the form element. - altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. - redaction: RedactionType, //Optional, Redaction Type to be applied to data, RedactionType.PLAIN_TEXT will be applied if not provided. -}; -``` -Note: If you don't provide a redaction type, RedactionType.PLAIN_TEXT will apply by default. - -The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data. But for reveal element, `inputStyles` accepts only `base` variant, `copyIcon` and `global` style objects. - -An example of a inputStyles object: - -```javascript -inputStyles: { - base: { - color: '#1d1d1d', - }, - copyIcon: { - position: 'absolute', - right: '8px', - top: 'calc(50% - 10px)', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -An example of a labelStyles object: - -```javascript -labelStyles: { - base: { - fontSize: '12px', - fontWeight: 'bold', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -An example of a errorTextStyles object: - -```javascript -errorTextStyles: { - base: { - color: '#f44336', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -}, -``` - -Along with RevealElementInput, you can define other options in the RevealElementOptions object as described below: -```js -const options = { - enableCopy: false, // Optional, enables the copy icon to reveal elements to copy text to clipboard. Defaults to 'false'). - format: String, // Optional, format for the element - translation: {} // Optional, indicates the allowed data type value for format. -} -``` - -`format`: A string value that indicates how the reveal element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified to any character in the `format` value is considered as a string literal. - -`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `{ ‘X’: ‘[0-9]’ }`. - -**Reveal Element Options examples:** -Example 1 -```js -const revealElementInput = { - token: '' -}; - -const options = { - format: '(XXX) XXX-XXXX', - translation: { 'X': '[0-9]'} -}; - -const revealElement = revealComposableContainer.create(revealElementInput,options); -``` - -Value from vault: "1234121234" -Revealed Value displayed in element: "(123) 412-1234" - -Example 2: -```js -const revealElementInput = { - token: '' -}; - -const options = { - format: 'XXXX-XXXXXX-XXXXX', - translation: { 'X': '[0-9]' } -}; - -const revealElement = revealComposableContainer.create(revealElementInput,options); -``` - -Value from vault: "374200000000004" -Revealed Value displayed in element: "3742-000000-00004" - -Once you've defined a Skyflow Element, you can use the `create(element)` method of the container to create the Element as shown below: - -```javascript -const element = revealComposableContainer.create(revealElement) -``` - -### Step 3: Mount Container to the DOM -To specify where the Elements are rendered on your page, create a placeholder `
    ` element with unique `id` attribute. Use this empty `
    ` placeholder to mount the composable reveal container. - -```javascript -
    -
    -
    -
    - - -``` -Use the composable container's `mount(domElement)` method to insert the container's Elements into the specified `
    `. For instance, the following call inserts Elements into the `
    ` with the `id "#composableContainer"`. - -```javacript -revealComposableContainer.mount('#composableRevealContainer'); -``` - -### Step 4: Reveal data -When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: - -```javascript -container - .reveal() - .then(data => { - // Handle success. - }) - .catch(err => { - // Handle error. - }); -``` - -### End to end example of reveal data with Composable Reveal Elements -```javascript -// Step 1. -const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); -// Step 2. -const cardNumberElement = container.create({ - token: 'b63ec4e0-bbad-4e43-96e6-6bd50f483f75', - inputStyles: { - base: { - color: '#1d1d1d', - }, - }, - labelStyles: { - base: { - fontSize: '12px', - }, - }, - errorTextStyles: { - base: { - color: '#f44336', - }, - }, - label: 'card_number', - altText: 'XXXX XXXX XXXX XXXX', - redaction: Skyflow.RedactionType.MASKED -}); - -const cvvElement = container.create({ - token: '89024714-6a26-4256-b9d4-55ad69aa4047', - inputStyles: { - base: { - color: '#1d1d1d', - }, - }, - label: 'cvv', - altText: 'XXX', -}); - -const expiryDate= container.create({ - token: 'a4b24714-6a26-4256-b9d4-55ad69aa4047', - inputStyles: { - base: { - color: '#1d1d1d', - }, - }, - label: 'expiryDate', - altText: 'MM/YYYY', -}); -// Step 3. -container.mount('#container') -// Step 4. -container - .reveal() - .then(data => { - // Handle success. - }) - .catch(err => { - // Handle error. - }); -``` -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. - -### Sample Response - -``` -{ - "success": [ - { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - "value": "xxxxxxxxx4163" - "valueType": "STRING" - }, - { - "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", - "value": "12/2098" - "valueType": "STRING" - } - ], - "errors": [ - { - "token": "89024714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047" - } - } - ] -} -``` - -## Update Reveal Composable Elements - -You can update reveal composable element properties with the `update` interface. - -The `update` interface takes the below object: -```javascript -const updateElement = { - token: 'string', // Optional, token of the data being revealed. - inputStyles: {}, // Optional, styles to be applied to the element. - labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. - label: 'string', // Optional, label for the form element. - altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. - redaction: RedactionType, // Optional, Redaction Type to be applied to data. - skyflowID: 'string', // Optional, Skyflow ID of the file to render. - table: 'string', // Optional, table name of the file to render. - column: 'string' // Optional, column name of the file to render. -}; -``` - -Only include the properties that you want to update for the specified reveal element. - -Properties your provided when you created the element remain the same until you explicitly update them. - - -### End to end example -```javascript -// Create a reveal composable container. -const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); - -const stylesOptions = { - inputStyles: { - base: { - fontFamily: 'Inter', - fontStyle: 'normal', - fontWeight: 400, - fontSize: '14px', - lineHeight: '21px', - width: '294px', - }, - }, - labelStyles: {}, - errorTextStyles: { - base: { - color: '#f44336' - }, - }, -}; - -// Create reveal elements -const cardHolderNameRevealElement = revealComposableContainer.create({ - token: 'ed5fdd1f-5009-435c-a06b-3417ce76d2c8', - altText: 'first name', - ...stylesOptions, - label: 'Card Holder Name', -}); - -const cardNumberRevealElement = revealComposableContainer.create({ - token: '8ee84061-7107-4faf-bb25-e044f3d191fe', - altText: 'xxxx', - ...stylesOptions, - label: 'Card Number', - redaction: 'RedactionType.CARD_NUMBER' -}); - -// Mount the reveal elements. -revealContainer.mount('#container'); // Assumes there is a div with container -// ... - -// Update label, labelStyles properties on cardHolderNameRevealElement. -cardHolderNameRevealElement.update({ - label: 'CARDHOLDER NAME', - labelStyles: { - base: { - color: '#aa11aa' - } - } -}); - -// Update inputStyles, errorTextStyles properties on cardNumberRevealElement. -cardNumberRevealElement.update({ - inputStyles: { - base: { - color: '#fff', - backgroundColor: '#000', - borderColor: '#f00', - borderWidth: '5px' - } - }, - errorTextStyles: { - base: { - backgroundColor: '#000', - } - } -}); -``` - ---- - - -## Render a file with a Composable File Element - -You can render files using the Skyflow File Element. Use the following steps to securely render a file. - -### Step 1: Create a container -Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: - -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions) -``` - -### Step 2: Create a File Element -Define a Skyflow Element to render the file as shown below. - -```javascript -const fileElement = { - inputStyles: {}, // Optional, styles to be applied to the element. - errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the render element. - altText: 'string', // Optional, string that is shown before file render call - skyflowID: 'string', // Required, skyflow id of the file to render - column: 'string', // Required, column name of the file to render - table: 'string', // Required, table name of the file to render -}; -``` -The inputStyles and errorTextStyles parameters accept a styles object as described in the [previous section](https://github.com/skyflowapi/skyflow-js#step-2-create-a-collect-element) for collecting data. But for render file elements, inputStyles accepts only base variant, global style objects. - -An example of a inputStyles object: - -```javascript -inputStyles: { - base: { - height: '400px', - width: '300px', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -} -``` -An example of a errorTextStyles object: -```javascript -errorTextStyles: { - base: { - color: '#f44336', - }, - global: { - '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - } -} -``` -### Step 3: Mount Container to the DOM -Mount Elements for file rendering to the DOM the same way as Elements used for revealing data. Refer to Step 3 of the [section above](#step-3-mount-container-to-the-dom). - -### Step 4: Render File -After you create and mount the element, call the renderFile() method on the element as shown below: -```javascript -fileElement - .renderFile() - .then(data => { - // Handle success. - }) - .catch(err => { - // Handle error. - }); -``` - -### End to end example of file render -```javascript -// Step 1. -const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); - -// REPLACE with your custom implementation to fetch skyflow_id from backend service. -// Sample implementation -fetch("") - .then((response) => { - - // on successful fetch skyflow_id - const skyflowID = response.skyflow_id; - - // Step 2. - const fileElement = container.create({ - skyflowID: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - column: "file", - table: "table", - inputStyles: { - base: { - height: "400px", - width: "300px", - }, - }, - errorTextStyles: { - base: { - color: "#f44336", - }, - }, - altText: "This is an altText", - }); - // Step 3. - fileElement.mount("#renderFile"); // Assumes there is a placeholder div with id=renderFile on the page - - const renderButton = document.getElementById("renderFiles"); // button to call render file - - if (renderButton) { - renderButton.addEventListener("click", () => { - - // Step 4. - fileElement - .renderFile() - .then((data) => { - // Handle success. - }) - .catch((err) => { - // Handle error. - }); - }); - } - }) - .catch((err) => { - // failed to fetch skyflow_id - console.log(err); - }); - -``` - -# Securely deleting data client-side -- [**Deleting data from the vault**](#deleting-data-from-the-vault) - -## Deleting data from the vault - -To delete data from the vault, use the `delete(records, options?)` method of the Skyflow client. The `records` parameter takes an array of records to delete in the following format. The `options` parameter is optional and takes an object of deletion parameters. Currently, there are no supported deletion parameters. - -```javascript -const records = [ - { - id: "", // skyflow id of the record to delete - table: "" // Table from which the record is to be deleted - }, - { - // ...additional records here - }, -], - -skyflowClient.delete(records); -``` - -An [example](https://github.com/skyflowapi/skyflow-js/blob/main/samples/using-script-tag/delete-pure-js.html) of delete call: - -```javascript -skyflowClient.delete({ - records: [ - { - id: "29ebda8d-5272-4063-af58-15cc674e332b", - table: "cards", - }, - { - id: "d5f4b926-7b1a-41df-8fac-7950d2cbd923", - table: "cards", - } - ], -}); -``` - -A sample response: - -```json -{ - "records": [ - { - "skyflow_id": "29ebda8d-5272-4063-af58-15cc674e332b", - "deleted": true, - }, - { - "skyflow_id": "29ebda8d-5272-4063-af58-15cc674e332b", - "deleted": true, - } - ] -} -``` - -# Set Custom Network messages on container: - -Add custom network error messages to a container with the `setError` method. - -`setError(ErrorMessages: Record)` sets the error text for the different network errors types. When this method is triggered, all the errors present in the error response are overridden with the specified custom error message. This error is sent on the collect or upload file call on the same container. - -### Sample code snippet for setError on collect container -```javascript -const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); - -const cardNumber = container.create({ - table: 'pii_fields', - column: 'primary_card.card_number', - type: Skyflow.ElementType.CARD_NUMBER, -}); - -// Set custom error. -container.setError({ - [Skyflow.ErrorType.BAD_REQUEST]: "Bad request. Please check the request payload.", - [Skyflow.ErrorType.UNAUTHORIZED]: "You are not authorized. Please check your token.", - [Skyflow.ErrorType.FORBIDDEN]: "Access denied. You do not have permission to perform this action.", - [Skyflow.ErrorType.TOO_MANY_REQUESTS]: "Too many requests. Please try again later.", - [Skyflow.ErrorType.INTERNAL_SERVER_ERROR]: "Something went wrong on our end. Please try again later.", - [Skyflow.ErrorType.BAD_GATEWAY]: "Received an invalid response from the server. Please try again.", - [Skyflow.ErrorType.SERVICE_UNAVAILABLE]: "Service is temporarily unavailable. Please try again later.", - [Skyflow.ErrorType.CONNECTION]: "Unable to connect to the server. Please check your network connection.", - [Skyflow.ErrorType.NOT_FOUND]: "Table not found with custom message", - [Skyflow.ErrorType.OFFLINE]: "You appear to be offline. Please check your internet connection.", - [Skyflow.ErrorType.TIMEOUT]: "The request took too long to respond. Please try again.", - [Skyflow.ErrorType.ABORT]: "The request was aborted.", - [Skyflow.ErrorType.NETWORK_GENERIC]: "A network error occurred. Please try again.", -}); - -container - .collect() - .then(res => console.log(res)) - .catch(err =>{ - console.log(err); -}) -``` -#### Sample Error structure: -```json -{ - "error":{ - "code":0, - "description":"You appear to be offline. Please check your internet connection.", - "type":"OFFLINE" - }, -} -``` - -`Skyflow.ErrorType` accepts following values: - - `BAD_REQUEST` - - `UNAUTHORIZED` - - `FORBIDDEN` - - `TOO_MANY_REQUESTS` - - `INTERNAL_SERVER_ERROR` - - `BAD_GATEWAY` - - `SERVICE_UNAVAILABLE` - - `CONNECTION` - - `NOT_FOUND` - - `OFFLINE` - - `TIMEOUT` - - `NETWORK_GENERIC` - - `ABORT` - - -## Reporting a Vulnerability - -If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. +Both SDKs expose the same `Skyflow` global when loaded via script tag, so a single page must load only one of them. If you need both in one app, install both from npm and alias on import. +## Repository layout +- `packages/skyflow-js/` — skyflow-js SDK ([README.md](packages/skyflow-js/README.md)) +- `packages/skyflow-flowvault-js/` — skyflow-flowvault-js SDK ([README.md](packages/skyflow-flowvault-js/README.md)) +- `core/` — shared internal source compiled into both SDKs (not installable on its own) +- `/samples/` — sample apps for each SDK diff --git a/core/api-utils/collect.ts b/core/api-utils/collect.ts new file mode 100644 index 000000000..e730b7914 --- /dev/null +++ b/core/api-utils/collect.ts @@ -0,0 +1,90 @@ +/* eslint-disable import/prefer-default-export */ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Variant-neutral collect request-assembly. `constructElementsInsertReq` builds +// the generic records/updateRecords from collected element values; each package +// then builds its own API request (privacyDB constructInsertRecordRequest, +// flowDB constructFlowDBInsertRequest). The privacyDB /v1 builders and transport +// stay in src/api-utils/collect and consume this helper. +import get from 'lodash/get'; +import { safeMerge } from '@core/utils/safe-merge'; +import { IInsertRecord } from '@core/types'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import SkyflowError from '@core/errors'; + +const keyify = (obj, prefix = '') => Object.keys(obj).reduce((res: any, el) => { + if (Array.isArray(obj[el])) { + return [...res, prefix + el]; + } if (typeof obj[el] === 'object' && obj[el] !== null) { + return [...res, ...keyify(obj[el], `${prefix + el}.`)]; + } + return [...res, prefix + el]; +}, []); + +// Exported so flowDB's variant-specific constructElementsInsertReq can reuse the +// same duplicate-column guard (its additionalFields shape differs, but this check +// is identical). `keyify` stays private — only this helper consumes it. +export const checkDuplicateColumns = (additionalColumns, columns, table) => { + const keys = keyify(additionalColumns); + keys.forEach((key) => { + const value = get(columns, key); + if (value) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, [`${key}`, `${table}`], true); + } + }); +}; + +export const constructElementsInsertReq = (req, update, options) => { + let tables = Object.keys(req); + let ids = Object.keys(update); + const additionalFields = options?.additionalFields; + if (additionalFields) { + // merge additionalFields in req + additionalFields.records.forEach((record) => { + if (record.fields.skyflowID) { + if (ids.includes(record.fields.skyflowID)) { + checkDuplicateColumns( + record.fields, update[record.fields.skyflowID], record.table, + ); + const temp = record.fields; + safeMerge(temp, update[record.fields.skyflowID]); + update[record.fields.skyflowID] = temp; + } else { + update[record.fields.skyflowID] = { + ...record.fields, + table: record.table, + }; + } + } else if (!record.fields.skyflowID) { + if (tables.includes(record.table)) { + checkDuplicateColumns(record.fields, req[record.table], record.table); + const temp = record.fields; + safeMerge(temp, req[record.table]); + req[record.table] = temp; + } else { + req[record.table] = record.fields; + } + } + }); + } + const records: IInsertRecord[] = []; + const updateRecords: IInsertRecord[] = []; + + tables = Object.keys(req); + tables.forEach((table) => { + records.push({ + table, + fields: req[table], + }); + }); + ids = Object.keys(update); + ids.forEach((id) => { + updateRecords.push({ + table: update[id].table, + fields: update[id], + skyflowID: id, + }); + }); + return [{ records }, { updateRecords }]; +}; diff --git a/core/api-utils/reveal.ts b/core/api-utils/reveal.ts new file mode 100644 index 000000000..b222cc5b1 --- /dev/null +++ b/core/api-utils/reveal.ts @@ -0,0 +1,56 @@ +/* eslint-disable import/prefer-default-export */ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Variant-neutral reveal formatter: groups response records by token for the +// element iframes. The privacyDB /v1 fetch/parse transport stays in +// src/api-utils/reveal and consumes this helper. +import { IRevealResponseType } from '@core/types'; +import SkyflowError from '@core/errors'; + +// Token-based generic failure formatter. Variant-neutral — moved here from each +// package's api-utils/reveal so both SDKs share one copy. purejs=true returns a +// plain { code, description }; otherwise the SkyflowError envelope is spread in. +export const formatForPureJsFailure = (cause, tokenId: string, purejs: boolean) => { + if (purejs) { + return { + token: tokenId, + error: { + code: cause?.error?.code, + description: cause?.error?.description, + }, + }; + } + return ({ + token: tokenId, + ...new SkyflowError({ + code: cause?.error?.code, + description: cause?.error?.description, + type: cause?.error?.type, + }, [], true), + }); +}; + +export const formatRecordsForIframe = (response: IRevealResponseType) => { + const result: Record = {}; + if (response.records) { + response.records.forEach((record) => { + const key = record.token; + const recordData = { + value: record.value, + redaction: record.redaction, + }; + + if (result[key]) { + if (Array.isArray(result[key])) { + result[key].push(recordData); + } else { + result[key] = [result[key], recordData]; + } + } else { + result[key] = recordData; + } + }); + } + return result; +}; diff --git a/src/client/index.ts b/core/client/index.ts similarity index 87% rename from src/client/index.ts rename to core/client/index.ts index 2f8745302..e09df6cad 100644 --- a/src/client/index.ts +++ b/core/client/index.ts @@ -1,17 +1,21 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { ContentType, SKY_METADATA_HEADER } from '../core/constants'; -import SkyflowError from '../libs/skyflow-error'; -import { ISkyflow } from '../skyflow'; -import SKYFLOW_ERROR_CODE from '../utils/constants'; -import logs from '../utils/logs'; -import sdkDetails from '../../package.json'; + +// Variant-neutral HTTP client (transport), lifted to @core (Task 4.7). The two +// packages shipped byte-identical clients apart from import sourcing; the body +// wires only @core (neutral SkyflowError base, constants, logs, types). The +// sky-metadata header's SDK identity comes from this bundle's build-injected +// SDK_NAME/SDK_VERSION globals (present in every build, including the iframe), +// passed to the shared @core getMetaObject — no runtime variant registry. +import { ContentType, SKY_METADATA_HEADER } from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import SkyflowError from '@core/errors'; +import { getMetaObject } from '@core/utils/metrics-helper'; import { - getMetaObject, -} from '../utils/helpers'; -import { ClientMetadata } from '../core/internal/internal-types'; -import { ErrorMessages, ErrorType } from '../utils/common'; + ISkyflow, ClientMetadata, ErrorMessages, ErrorType, +} from '@core/types'; export interface IClientRequest { body?: Document | XMLHttpRequestBodyInit | null; @@ -85,7 +89,11 @@ class Client { httpRequest.open(request.requestMethod, request.url); if (request.headers) { - const metaDataObject = getMetaObject(sdkDetails, this.#metaData, navigator); + const metaDataObject = getMetaObject( + { name: SDK_NAME, version: SDK_VERSION }, + this.#metaData, + navigator, + ); request.headers[SKY_METADATA_HEADER] = JSON.stringify(metaDataObject); const headers = request.headers; Object.keys(request.headers).forEach((key) => { diff --git a/src/core/constants.ts b/core/constants.ts similarity index 80% rename from src/core/constants.ts rename to core/constants.ts index 80899fd5b..aed457edb 100644 --- a/src/core/constants.ts +++ b/core/constants.ts @@ -1,26 +1,28 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import defaultCardIcon from '../../assets/default.svg'; -import amexIcon from '../../assets/amex.svg'; -import dinnersClubIcon from '../../assets/diners-club.svg'; -import discoverIcon from '../../assets/discover.svg'; -import hipperCardIcon from '../../assets/hipercard.svg'; -import jcbIcon from '../../assets/jcb.svg'; -import maestroIcon from '../../assets/maestro.svg'; -import maseterCardIcon from '../../assets/mastercard.svg'; -import unionPayIcon from '../../assets/unionpay.svg'; -import visaCardIcon from '../../assets/visa.svg'; -import copyIcon from '../../assets/copyIcon.svg'; -import successIcon from '../../assets/path.svg'; -import dropDownIcon from '../../assets/drop-down.svg'; -import cartesBancairesIcon from '../../assets/carter-banceris.svg'; - -import logs from '../utils/logs'; +import logs from '@core/utils/logs'; +import defaultCardIcon from '../assets/default.svg'; +import amexIcon from '../assets/amex.svg'; +import dinnersClubIcon from '../assets/diners-club.svg'; +import discoverIcon from '../assets/discover.svg'; +import hipperCardIcon from '../assets/hipercard.svg'; +import jcbIcon from '../assets/jcb.svg'; +import maestroIcon from '../assets/maestro.svg'; +import maseterCardIcon from '../assets/mastercard.svg'; +import unionPayIcon from '../assets/unionpay.svg'; +import visaCardIcon from '../assets/visa.svg'; +import copyIcon from '../assets/copyIcon.svg'; +import successIcon from '../assets/path.svg'; +import dropDownIcon from '../assets/drop-down.svg'; +import cartesBancairesIcon from '../assets/carter-banceris.svg'; export const SESSION_ID = 'session_id'; export const SKY_METADATA_HEADER = 'sky-metadata'; -export const SDK_VERSION = 'sdkVersion'; +// Metadata object KEY under which the SDK version string is stored (serialized into +// the sky-metadata header). Distinct from the build-injected `SDK_VERSION` DefinePlugin +// global (the actual package version) — do not conflate the two. +export const SDK_VERSION_KEY = 'sdkVersion'; export const COLLECT_FRAME_CONTROLLER = 'collect_controller'; export const REVEAL_FRAME_CONTROLLER = 'reveal_controller'; export const SKYFLOW_FRAME_CONTROLLER = 'skyflow_controller'; @@ -166,7 +168,13 @@ export const ELEMENT_EVENTS_TO_CONTAINER = { RENDER_FILE_REQUEST: 'RENDER_FILE_REQUEST', }; -export enum ElementType { +// Base element types supported by every variant (flowDB + privacyDB). This is the +// shared @core base; file elements are a privacyDB-only extension (see +// FileElementType) — flowDB has no file support — so they are intentionally NOT +// part of this base. Neither BaseElementType nor FileElementType is public: each +// package defines its own public `ElementType` on top of these — privacyDB as +// base + file, flowvault as base only. +export enum BaseElementType { CVV = 'CVV', EXPIRATION_DATE = 'EXPIRATION_DATE', CARD_NUMBER = 'CARD_NUMBER', @@ -175,10 +183,21 @@ export enum ElementType { PIN = 'PIN', EXPIRATION_MONTH = 'EXPIRATION_MONTH', EXPIRATION_YEAR = 'EXPIRATION_YEAR', +} + +// File element types — the privacyDB-only extension of the base (flowDB has no +// file upload/render support). Defined in @core because the shared collect +// pipeline (iframe-form file handling, collect-element file metadata) references +// these values; not public — privacyDB folds them into its own `ElementType`. +export enum FileElementType { FILE_INPUT = 'FILE_INPUT', MULTI_FILE_INPUT = 'MULTI_FILE_INPUT', } +// Any element type the shared pipeline may handle (base + file). Used internally +// where a field can hold either set; not part of any package's public surface. +export type AnyElementType = BaseElementType | FileElementType; + export enum CardType { VISA = 'VISA', MASTERCARD = 'MASTERCARD', @@ -252,7 +271,7 @@ export const ELEMENTS = { }, sensitive: false, }, - [ElementType.CARDHOLDER_NAME]: { + [BaseElementType.CARDHOLDER_NAME]: { name: 'cardHolderName', attributes: { type: 'text', @@ -261,7 +280,7 @@ export const ELEMENTS = { sensitive: true, regex: /^([a-zA-Z\\ \\,\\.\\-\\']{2,})$/, }, - [ElementType.CARD_NUMBER]: { + [BaseElementType.CARD_NUMBER]: { name: 'CARD_NUMBER', attributes: { type: 'text', @@ -272,7 +291,7 @@ export const ELEMENTS = { mask: CARD_NUMBER_MASK[CardType.DEFAULT], regex: /$|^[\s]*?([0-9]{2,6}[ -]?){3,5}[\s]*/, }, - [ElementType.EXPIRATION_DATE]: { + [BaseElementType.EXPIRATION_DATE]: { name: 'EXPIRATION_DATE', attributes: { type: 'text', @@ -283,7 +302,7 @@ export const ELEMENTS = { // mask: ["XY/YYYY", { X: "[0-1]", Y: "[0-9]" }], // regex: /^(0[1-9]|1[0-2])\/([0-9]{4})$/, }, - [ElementType.EXPIRATION_MONTH]: { + [BaseElementType.EXPIRATION_MONTH]: { name: 'EXPIRATION_MONTH', attributes: { maxLength: 2, @@ -294,7 +313,7 @@ export const ELEMENTS = { sensitive: true, mask: ['XX', { X: '[0-9]' }], }, - [ElementType.EXPIRATION_YEAR]: { + [BaseElementType.EXPIRATION_YEAR]: { name: 'EXPIRATION_YEAR', attributes: { // maxLength: 4, @@ -304,7 +323,7 @@ export const ELEMENTS = { }, sensitive: true, }, - [ElementType.CVV]: { + [BaseElementType.CVV]: { name: 'CVV', attributes: { type: 'text', @@ -314,14 +333,14 @@ export const ELEMENTS = { sensitive: true, regex: /^$|^[0-9]{3,4}$/, }, - [ElementType.INPUT_FIELD]: { + [BaseElementType.INPUT_FIELD]: { name: 'INPUT_FIELD', sensitive: true, attributes: { type: 'text', }, }, - [ElementType.PIN]: { + [BaseElementType.PIN]: { name: 'PIN', attributes: { type: 'text', @@ -332,14 +351,14 @@ export const ELEMENTS = { sensitive: true, regex: /^$|^[0-9]{4,12}$/, }, - [ElementType.FILE_INPUT]: { + [FileElementType.FILE_INPUT]: { name: 'FILE_INPUT', sensitive: true, attributes: { type: 'file', }, }, - [ElementType.MULTI_FILE_INPUT]: { + [FileElementType.MULTI_FILE_INPUT]: { name: 'MULTI_FILE_INPUT', sensitive: true, attributes: { @@ -645,36 +664,36 @@ export enum ContentType { } export const ALLOWED_FOCUS_AUTO_SHIFT_ELEMENT_TYPES = [ - ElementType.CARD_NUMBER, - ElementType.EXPIRATION_DATE, - ElementType.EXPIRATION_MONTH, - ElementType.EXPIRATION_YEAR, + BaseElementType.CARD_NUMBER, + BaseElementType.EXPIRATION_DATE, + BaseElementType.EXPIRATION_MONTH, + BaseElementType.EXPIRATION_YEAR, ]; export const DEFAULT_ERROR_TEXT_ELEMENT_TYPES = { - [ElementType.CVV]: 'Invalid cvv', - [ElementType.EXPIRATION_DATE]: 'Invalid expiration date', - [ElementType.CARD_NUMBER]: 'Invalid card number', - [ElementType.CARDHOLDER_NAME]: 'Invalid cardholder name', - [ElementType.INPUT_FIELD]: logs.errorLogs.INVALID_COLLECT_VALUE, - [ElementType.PIN]: 'Invalid pin', - [ElementType.EXPIRATION_MONTH]: 'Invalid expiration month', - [ElementType.EXPIRATION_YEAR]: 'Invalid expiration year', - [ElementType.FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, - [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, + [BaseElementType.CVV]: 'Invalid cvv', + [BaseElementType.EXPIRATION_DATE]: 'Invalid expiration date', + [BaseElementType.CARD_NUMBER]: 'Invalid card number', + [BaseElementType.CARDHOLDER_NAME]: 'Invalid cardholder name', + [BaseElementType.INPUT_FIELD]: logs.errorLogs.INVALID_COLLECT_VALUE, + [BaseElementType.PIN]: 'Invalid pin', + [BaseElementType.EXPIRATION_MONTH]: 'Invalid expiration month', + [BaseElementType.EXPIRATION_YEAR]: 'Invalid expiration year', + [FileElementType.FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, + [FileElementType.MULTI_FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, }; export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { - [ElementType.CVV]: 'cvv is required', - [ElementType.EXPIRATION_DATE]: 'expiration date is required', - [ElementType.CARD_NUMBER]: 'card number is required', - [ElementType.CARDHOLDER_NAME]: 'cardholder name is required', - [ElementType.INPUT_FIELD]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, - [ElementType.PIN]: 'pin is required', - [ElementType.EXPIRATION_MONTH]: 'expiration month is required', - [ElementType.EXPIRATION_YEAR]: 'expiration year is required', - [ElementType.FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, - [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, + [BaseElementType.CVV]: 'cvv is required', + [BaseElementType.EXPIRATION_DATE]: 'expiration date is required', + [BaseElementType.CARD_NUMBER]: 'card number is required', + [BaseElementType.CARDHOLDER_NAME]: 'cardholder name is required', + [BaseElementType.INPUT_FIELD]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, + [BaseElementType.PIN]: 'pin is required', + [BaseElementType.EXPIRATION_MONTH]: 'expiration month is required', + [BaseElementType.EXPIRATION_YEAR]: 'expiration year is required', + [FileElementType.FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, + [FileElementType.MULTI_FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, }; export const INPUT_KEYBOARD_EVENTS = { @@ -687,11 +706,11 @@ export const INPUT_KEYBOARD_EVENTS = { export const CUSTOM_ROW_ID_ATTRIBUTE = 'data-row-id'; export const INPUT_FORMATTING_NOT_SUPPORTED_ELEMENT_TYPES = [ - ElementType.CARDHOLDER_NAME, - ElementType.EXPIRATION_MONTH, - ElementType.FILE_INPUT, - ElementType.PIN, - ElementType.CVV, + BaseElementType.CARDHOLDER_NAME, + BaseElementType.EXPIRATION_MONTH, + FileElementType.FILE_INPUT, + BaseElementType.PIN, + BaseElementType.CVV, ]; export const DEFAULT_CARD_NUMBER_SEPERATOR = ' '; diff --git a/src/custom.d.ts b/core/custom.d.ts similarity index 100% rename from src/custom.d.ts rename to core/custom.d.ts diff --git a/src/libs/skyflow-error.ts b/core/errors/index.ts similarity index 87% rename from src/libs/skyflow-error.ts rename to core/errors/index.ts index ddba42a2e..9ea2d71ee 100644 --- a/src/libs/skyflow-error.ts +++ b/core/errors/index.ts @@ -1,8 +1,8 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { ErrorType } from '../utils/common'; -import { parameterizedString } from '../utils/logs-helper'; +import { ErrorType } from '@core/types'; +import { parameterizedString } from '@core/utils/logs-helper'; export interface ISkyflowError{ code:string | number, diff --git a/src/event-emitter/index.ts b/core/event-emitter/index.ts similarity index 100% rename from src/event-emitter/index.ts rename to core/event-emitter/index.ts diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts new file mode 100644 index 000000000..b3343c301 --- /dev/null +++ b/core/external/base-skyflow.ts @@ -0,0 +1,430 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Shared public-entry base for the SDK `Skyflow` class. Both packages shipped +// near-identical entry classes (~75% byte-identical); everything that did not +// actually differ lives here now: +// - the constructor (uuid/session metadata, Client construction, controller +// container bootstrap, the framebus GET_BEARER_TOKEN listener, startup logs) +// - `static init()` — URL normalization + custom-elements-URL handling +// - `getSkyflowBearerToken()` — the promise-shaped token accessor threaded into +// every container +// - `container()` — the four overloads and the shared switch/props assembly +// - the ten static enum getters whose values are the same `@core` objects in +// both packages +// +// Divergence is injected through five abstract factory hooks (which concrete +// container class to `new`), exactly as the container bases in this folder do. +// Boundary-clean: this file names only `@core` types and the type parameters' +// bounds — never a `packages/*` class. Core calls into the subclass purely by +// dynamic dispatch on the abstract hooks. +// +// Generic parameters exist so `container()`'s overloads — written once here — +// still return each package's own container classes at the call site. Each is +// bounded to the matching `@core` container base class (CoreCollectContainer, +// CoreRevealContainer, …) — the shared supertype both packages extend — so a +// subclass can only wire a container of the right family and no `packages/*` +// class is named here. +// +// What deliberately stays in each package's `skyflow.ts` subclass: +// - the five `instantiate*`/`create*Container` hooks +// - `static get Error()` — SkyflowError vs SkyflowFlowDBError +// - privacyDB's pure-JS API (insert/detokenize/get/getById/delete/update) and +// `static get ThreeDS()`; flowDB's `static get UpdateType()` +// - the `setVariantAdapter()` registration call +import bus from 'framebus'; +import uuid from '@core/libs/uuid'; +import isTokenValid from '@core/utils/jwt-utils'; +import { + CardType, + ELEMENT_EVENTS_TO_IFRAME, + SDK_VERSION_KEY, + SESSION_ID, +} from '@core/constants'; +import properties from '@core/properties'; +import logs from '@core/utils/logs'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import SkyflowError from '@core/errors'; +import Client from '@core/client'; +import CoreSkyflowContainer from '@core/external/skyflow-container'; +import CoreCollectContainer from '@core/external/collect/collect-container'; +import CoreRevealContainer from '@core/external/reveal/reveal-container'; +import CoreComposableCollectContainer from '@core/external/collect/composable-collect-container'; +import CoreComposableRevealContainer from '@core/external/reveal/composable-reveal-container'; +import type CoreRevealElement from '@core/external/reveal/reveal-element'; +import { checkAndSetForCustomUrl, formatVaultURL } from '@core/helpers'; +import { validateComposableContainerOptions } from '@core/validators'; +import { printLog, parameterizedString, getStoredSdkVersion } from '@core/utils/logs-helper'; +import { + ClientMetadata, + CollectElementInput, + ContainerOptions, + ContainerType, + Context, + Env, + ErrorType, + EventName, + ICollectElementOptionsBase, + ICollectElementUpdateOptionsBase, + ICollectOptionsBase, + ICollectResponseBase, + ICoreMetadata, + IRevealInputBase, + IRevealOptionsBase, + IRevealResponseBase, + ISkyflow, + LogLevel, + MessageType, + RedactionType, + ValidationRuleType, +} from '@core/types'; + +const CLASS_NAME = 'Skyflow'; + +// The container-generic bounds each subclass must satisfy, extracted to named +// aliases so the class header stays readable and the marker lists live in one +// place. They use the shared `@core` marker bases (ICollectOptionsBase / +// ICollectResponseBase / ICollectElementUpdateOptionsBase / IRevealInputBase / +// IRevealOptionsBase / IRevealResponseBase) rather than `any` — so a subclass can +// only wire a container of the right family AND the right input/option/response +// contract. Package option/response types are never named here (that would cross +// the @core ⇏ packages line); the markers are their common supertype, which is all +// BaseSkyflow needs. +type CollectContainerContract = CoreCollectContainer< +ICollectOptionsBase, ICollectResponseBase, ICollectElementUpdateOptionsBase, +CollectElementInput, ICollectElementOptionsBase +>; +// The reveal options slots read `void | IRevealOptionsBase` because privacyDB binds +// `TRevealOptions = void` (it has no reveal options) while flowDB binds an object: +// `void` is not assignable to the empty marker, but the union admits both, and +// `reveal(options?)` is a method so the type argument is checked bivariantly — no +// package-side change needed. +type RevealContainerContract = CoreRevealContainer< +IRevealInputBase, void | IRevealOptionsBase, IRevealResponseBase, +CoreRevealElement +>; +type ComposableCollectContract = CoreComposableCollectContainer< +ICollectOptionsBase, ICollectResponseBase, CollectElementInput, ICollectElementOptionsBase +>; +type ComposeRevealContract = CoreComposableRevealContainer< +void | IRevealOptionsBase, IRevealResponseBase +>; + +abstract class BaseSkyflow< + TSkyflowContainer extends CoreSkyflowContainer, + TCollectContainer extends CollectContainerContract, + TRevealContainer extends RevealContainerContract, + TComposableContainer extends ComposableCollectContract, + TComposeRevealContainer extends ComposeRevealContract, +> { + // `protected` (not `#private`) because the subclasses reach these: privacyDB's + // pure-JS methods delegate to `skyflowContainer`. `#uuid`/`#bearerToken` are + // read only in here, so they stay hard-private. + protected client: Client; + + #uuid: string = uuid(); + + protected metadata: ClientMetadata = { + uuid: this.#uuid, + clientDomain: window.location.origin, + }; + + protected skyflowContainer: TSkyflowContainer; + + #bearerToken: string = ''; + + protected logLevel: LogLevel; + + protected env: Env; + + constructor(config: ISkyflow) { + const localSDKversion = getStoredSdkVersion(); + this.metadata[SDK_VERSION_KEY] = localSDKversion; + this.metadata[SESSION_ID] = uuid(); + this.client = new Client( + { + ...config, + }, + this.metadata, + ); + this.logLevel = config?.options?.logLevel || LogLevel.ERROR; + this.env = config?.options?.env || Env.PROD; + // Prototype-method dispatch, so it resolves to the subclass override even + // though we are still inside the base constructor. The hook must therefore be + // implemented as a method, never as an arrow-function class field (those + // initialize after `super()` and would still be undefined here). + this.skyflowContainer = this.instantiateSkyflowContainer( + this.client, + { logLevel: this.logLevel, env: this.env }, + ); + + const cb = (data, callback: Function) => { + printLog(parameterizedString(logs.infoLogs.CAPTURED_BEARER_TOKEN_EVENT, CLASS_NAME), + MessageType.LOG, + this.logLevel); + if ( + this.client.config.getBearerToken + && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) + ) { + this.client.config + .getBearerToken() + .then((bearerToken) => { + if (isTokenValid(bearerToken)) { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.logLevel); + this.#bearerToken = bearerToken; + callback({ authToken: this.#bearerToken }); + } else { + printLog(parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), MessageType.ERROR, this.logLevel); + callback({ + error: parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), + }); + } + }) + .catch((err) => { + printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, + this.logLevel); + callback({ error: err }); + }); + } else { + printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), + MessageType.LOG, + this.logLevel); + callback({ authToken: this.#bearerToken }); + } + }; + + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN + this.#uuid, cb); + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_LISTENER, CLASS_NAME), MessageType.LOG, + this.logLevel); + printLog(parameterizedString(logs.infoLogs.CURRENT_ENV, CLASS_NAME, this.env), + MessageType.LOG, this.logLevel); + printLog(parameterizedString(logs.infoLogs.CURRENT_LOG_LEVEL, CLASS_NAME, this.logLevel), + MessageType.LOG, this.logLevel); + } + + // Written once, but still returns the *concrete* package class: the + // polymorphic `this` parameter binds to `typeof Skyflow` at the call site, so + // `Skyflow.init(config)` types as that package's `Skyflow` and `new this(...)` + // constructs it. `BaseSkyflow.init(...)` is rejected — an abstract constructor + // is not assignable to the `new (...) => T` bound. + static init(this: new (config: ISkyflow) => T, config: ISkyflow): T { + const logLevel = config?.options?.logLevel || LogLevel.ERROR; + checkAndSetForCustomUrl(config); + printLog(parameterizedString(logs.infoLogs.INITIALIZE_CLIENT, CLASS_NAME), MessageType.LOG, + logLevel); + + const tempConfig = config; + tempConfig.vaultURL = formatVaultURL(config.vaultURL); + const skyflow = new this(tempConfig); + printLog(parameterizedString(logs.infoLogs.CLIENT_INITIALIZED, CLASS_NAME), + MessageType.LOG, logLevel); + return skyflow; + } + + protected getSkyflowBearerToken: () => Promise = () => new Promise((resolve, reject) => { + if ( + this.client.config.getBearerToken + && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) + ) { + this.client.config + .getBearerToken() + .then((bearerToken) => { + if (isTokenValid(bearerToken)) { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.logLevel); + this.#bearerToken = bearerToken; + resolve(this.#bearerToken); + } else { + printLog(parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), MessageType.ERROR, this.logLevel); + reject({ + error: parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), + }); + } + }) + .catch((err) => { + printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, + this.logLevel); + reject({ error: err }); + }); + } else { + printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), + MessageType.LOG, + this.logLevel); + resolve(this.#bearerToken); + } + }); + + // ---- Injected divergence (see class doc) ---------------------------------- + // Each hook is the minimal "which concrete class do I `new`" decision. They + // are prototype methods, not arrow-function fields: `instantiateSkyflowContainer` + // is invoked from the base constructor, and keeping the whole set consistent + // avoids the field-initialization-order trap. + + protected abstract instantiateSkyflowContainer( + client: Client, + context: Context, + ): TSkyflowContainer; + + protected abstract createCollectContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): TCollectContainer; + + protected abstract createRevealContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): TRevealContainer; + + protected abstract createComposableContainer( + metaData: ICoreMetadata, + context: Context, + options: ContainerOptions, + ): TComposableContainer; + + protected abstract createComposeRevealContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): TComposeRevealContainer; + + // -------------------------------------------------------------------------- + + #containerProps = (type: ContainerType): ICoreMetadata => ({ + ...this.metadata, + clientJSON: this.client.toJSON(), + containerType: type, + skyflowContainer: this.skyflowContainer, + getSkyflowBearerToken: this.getSkyflowBearerToken, + }); + + #context = (): Context => ({ logLevel: this.logLevel, env: this.env }); + + container(type: ContainerType.COLLECT, options?: ContainerOptions): TCollectContainer; + container(type: ContainerType.REVEAL, options?: ContainerOptions): TRevealContainer; + container(type: ContainerType.COMPOSABLE, options?: ContainerOptions): TComposableContainer; + container(type: ContainerType.COMPOSE_REVEAL, + options?: ContainerOptions) + : TComposeRevealContainer; + container(type: ContainerType, options?: ContainerOptions) { + switch (type) { + case ContainerType.COLLECT: { + const collectContainer = this.createCollectContainer( + this.#containerProps(type), + this.#context(), + options, + ); + printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.logLevel); + return collectContainer; + } + case ContainerType.REVEAL: { + const revealContainer = this.createRevealContainer( + this.#containerProps(type), + this.#context(), + options, + ); + printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.logLevel); + return revealContainer; + } + case ContainerType.COMPOSABLE: { + validateComposableContainerOptions(options!); + const composableContainer = this.createComposableContainer( + this.#containerProps(type), + this.#context(), + options!, + ); + printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.logLevel); + return composableContainer; + } + + case ContainerType.COMPOSE_REVEAL: { + validateComposableContainerOptions(options!); + const revealComposableContainer = this.createComposeRevealContainer( + this.#containerProps(type), + this.#context(), + options, + ); + printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.logLevel); + return revealComposableContainer; + } + + default: + if (!type) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_CONTAINER_TYPE, [], true); + } + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_CONTAINER_TYPE, [type], true); + } + } + + // Static members inherit through the prototype chain, so these are exposed + // unchanged on each package's `Skyflow`. Both packages re-exported the very + // same `@core` objects here, so there is nothing variant-specific to keep in + // the subclasses — except `Error` (SkyflowError vs SkyflowFlowDBError), which + // each package defines itself, and their package-only additions. + + static get ContainerType() { + return ContainerType; + } + + // `ElementType` is NOT exposed here: it is the one enum that differs per package + // (privacyDB = base + file elements, flowvault = base only). Each package's + // Skyflow subclass defines its own `static get ElementType()` returning its + // public ElementType, so the shared base stays file-agnostic. + + static get RedactionType() { + return RedactionType; + } + + static get ErrorType() { + return ErrorType; + } + + // RequestMethod is deliberately NOT exposed on the shared base: it advertises + // connection/gateway capability (invokeConnection / invokeGateway) that only + // privacyDB has. The privacyDB `Skyflow` subclass re-declares this getter; + // flowDB (elements-only) inherits the base without it. See audit finding F1. + + static get LogLevel() { + return LogLevel; + } + + static get EventName() { + return EventName; + } + + static get Env() { + return Env; + } + + static get ValidationRuleType() { + return ValidationRuleType; + } + + static get CardType() { + return CardType; + } +} + +export default BaseSkyflow; diff --git a/core/external/collect/collect-container.ts b/core/external/collect/collect-container.ts new file mode 100644 index 000000000..3c7b43875 --- /dev/null +++ b/core/external/collect/collect-container.ts @@ -0,0 +1,482 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import bus from 'framebus'; +import deepClone from '@core/libs/deep-clone'; +import uuid from '@core/libs/uuid'; +import EventEmitter from '@core/event-emitter'; +import iframer, { setAttributes, getIframeSrc, setStyles } from '@core/iframe-libs/iframer'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import { + COLLECT_FRAME_CONTROLLER, + CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, + ELEMENTS, FRAME_ELEMENT, + COLLECT_TYPES, + AnyElementType, +} from '@core/constants'; +import properties from '@core/properties'; +import Container from '@core/external/common/container'; +import SkyflowError from '@core/errors'; +import { + validateElementOptions, + formatValidations, + formatOptions, +} from '@core/libs/element-options'; +import CollectElement from '@core/external/collect/collect-element'; +import { + ContainerType, Context, MessageType, + CollectElementInput, + ICollectElementOptionsBase, + ContainerOptions, + ErrorType, + ICoreMetadata, + ICollectOptionsBase, + ICollectResponseBase, + ICollectElementUpdateOptionsBase, + VariantCollectAdapter, +} from '@core/types'; +import { printLog, parameterizedString } from '@core/utils/logs-helper'; +import { + validateInitConfig, + validateAdditionalFieldsInCollect, + validateUpsertOptions, + validateBooleanOptions, +} from '@core/validators'; + +// Variant-neutral collect-element descriptor base. Omits the identity key that +// diverges by package — privacyDB `table` vs flowDB `tableName` — which each +// package's own `ICollectElement extends ICollectElementBase` adds. `column` is +// the shared column key. +export interface ICollectElementBase { + elementType: AnyElementType; + elementName: string; + name: string; + column?: string; + sensitive?: boolean; + replacePattern?: RegExp; + mask?: string[]; + value?: string; + isMounted: boolean; + [key: string]: unknown; +} + +export interface ElementGroupItem extends CollectElementInput, ICollectElementOptionsBase { + elementType: AnyElementType; + name?: string; + accept?: string[]; + elementName?: string; + // Internal identity keys. `table` is canonical (both packages remap to it in + // create()); the skyflow-id key is variant (privacyDB `skyflowID` / flowDB + // `skyflowId`) and read via getVariantAdapter().collect.skyflowIdKey. `tableName` + // may linger from the flowDB input spread before the tableName→table remap. + table?: string; + skyflowID?: string; + skyflowId?: string; + tableName?: string; +} + +export interface ElementGroup { + rows: Array<{ + elements: Array; + }>; +} + +const CLASS_NAME = 'CollectContainer'; +// Shared collect-container base. Owns the controller-frame bootstrap, the +// element lifecycle (create + createMultipleElement + stale-element cleanup) and +// the collect() orchestration common to both SDKs. Generic over the collect() +// options (TOptions) and response (TResponse), plus the public create() input +// (TCreateInput) and options (TCreateOptions) so each package keeps its own +// consumer-facing typing (privacyDB table/skyflowID + file options, flowDB +// tableName/skyflowId). Divergence is injected via hooks: validateCollectOptions +// (token handling) and wrapCollectError (error mapping); the create() input +// validator (validateCreateInput) and its one variant identity field +// (buildCreateElementFields — privacyDB `accept`, flowDB `table`); the +// skyflowID vs skyflowId wire key is read from the VariantAdapter. Only the +// privacyDB-only uploadFiles stays in the subclass. The element interfaces are +// defined here and re-exported by each package's subclass (imported as +// './collect-container' by compose-collect). +abstract class CollectContainer< + TOptions extends ICollectOptionsBase, + TResponse extends ICollectResponseBase, + TUpdateOptions extends ICollectElementUpdateOptionsBase, + TCreateInput extends CollectElementInput, + TCreateOptions extends ICollectElementOptionsBase, +> extends Container { + protected containerId: string; + + // Package-specific collect key strategy (privacyDB vs flowDB `skyflowId`/`table` + // naming). Abstract — each package MUST supply it; there is no default, so a + // missing implementation is a compile error, not a silent privacyDB fallback. + // Read here (skyflowIdKey) and injected into every CollectElement this container + // builds, replacing the former global getVariantAdapter().collect lookup. + protected abstract collectVariant: VariantCollectAdapter; + + protected elements: Record> = {}; + + protected metaData: ICoreMetadata; + + protected context: Context; + + type:string = ContainerType.COLLECT; + + #eventEmitter: EventEmitter; + + #isMounted: boolean = false; + + protected isSkyflowFrameReady: boolean = false; + + protected customErrorMessages: Partial> = {}; + + constructor( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ) { + super(); + this.isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; + this.containerId = uuid(); + this.metaData = { + ...metaData, + clientJSON: { + ...metaData.clientJSON, + config: { + ...metaData.clientJSON.config, + options: { + ...metaData.clientJSON.config?.options, + ...options, + }, + }, + }, + }; + this.context = context; + this.#eventEmitter = new EventEmitter(); + + const clientDomain = this.metaData.clientDomain || ''; + const iframe = iframer({ + name: `${COLLECT_FRAME_CONTROLLER}:${this.containerId}:${this.context.logLevel}:${btoa(clientDomain)}`, + referrer: clientDomain, + }); + setAttributes(iframe, { + src: getIframeSrc(), + }); + setStyles(iframe, { ...CONTROLLER_STYLES }); + printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + + this.#isMounted = true; + } + + // Shared create() orchestration. The two packages differed only in the input + // validator and a single identity field on the element descriptor, both now + // injected via hooks (validateCreateInput / buildCreateElementFields), so the + // body is single-sourced here. Typed over TCreateInput/TCreateOptions so each + // package's public signature keeps its own input/options keys. + create = ( + input: TCreateInput, + options: TCreateOptions = { required: false } as TCreateOptions, + ): CollectElement => { + this.validateCreateInput(input); + const validations = formatValidations(input.validations); + const formattedOptions = formatOptions(input.type, options, this.context.logLevel); + + const elementGroup: ElementGroup = { + rows: [{ + elements: [{ + elementType: input.type, + name: input.column, + // Hook-provided fields (privacyDB `accept`, flowDB `table`) are spread + // BEFORE `...input` so an explicit input key takes precedence — matching + // the 2.7.9 baseline order. + ...this.buildCreateElementFields(input, options), + ...input, + ...formattedOptions, + validations, + }], + }], + }; + + return this.createMultipleElement(elementGroup, true); + }; + + setError(errors: Partial>) { + this.customErrorMessages = errors; + } + + protected createMultipleElement = ( + multipleElements: ElementGroup, + isSingleElementAPI: boolean = false, + ): CollectElement => { + const elements: any[] = []; + const tempElements = deepClone(multipleElements); + + tempElements.rows.forEach((row) => { + row.elements.forEach((element) => { + const options = element; + const { elementType } = options; + validateElementOptions(elementType, options); + + options.sensitive = options.sensitive || ELEMENTS[elementType].sensitive; + options.replacePattern = options.replacePattern || ELEMENTS[elementType].replacePattern; + options.mask = options.mask || ELEMENTS[elementType].mask; + + // options.elementName = `${options.table}.${options.name}:${btoa(uuid())}`; + // options.elementName = (options.table && options.name) ? `${options.elementType}:${btoa( + // options.elementName, + // )}` : `${options.elementType}:${btoa(uuid())}`; + + options.isMounted = false; + + if ( + options.elementType === ELEMENTS.radio.name + || options.elementType === ELEMENTS.checkbox.name + ) { + options.elementName = `${options.elementName}:${btoa(options.value)}`; + } + + options.elementName = `${FRAME_ELEMENT}:${options.elementType}:${btoa(uuid())}`; + options.label = element.label; + // skyflowID (privacyDB) vs skyflowId (flowDB) is a wire-key contract; the + // active key comes from the registered VariantAdapter. + options.skyflowID = element[this.collectVariant.skyflowIdKey]; + + elements.push(options); + }); + }); + + tempElements.elementName = isSingleElementAPI + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(tempElements.name)}`; + + if ( + isSingleElementAPI + && !this.elements[elements[0].elementName] + && this.#hasElementName(elements[0].name) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + } + + let element = this.elements[tempElements.elementName]; + if (element) { + if (isSingleElementAPI) { + element.updateElementGroup(elements[0]); + } else { + element.updateElementGroup(tempElements); + } + } else { + const elementId = uuid(); + element = new CollectElement( + elementId, + tempElements, + this.metaData, + { + containerId: this.containerId, + isMounted: this.#isMounted, + type: this.type, + }, + isSingleElementAPI, + this.#destroyCallback, + this.#updateCallback, + this.context, + this.collectVariant, + this.#eventEmitter, + ); + this.elements[tempElements.elementName] = element; + } + + if (!isSingleElementAPI) { + elements.forEach((iElement) => { + const name = iElement.elementName; + if (!this.elements[name]) { + this.elements[name] = this.create(iElement.elementType, iElement); + } else { + this.elements[name].updateElementGroup(iElement); + } + }); + } + return element; + }; + + #removeElement = (elementName: string) => { + Object.keys(this.elements).forEach((element) => { + if (element === elementName) delete this.elements[element]; + }); + }; + + #destroyCallback = (elementNames: string[]) => { + elementNames.forEach((elementName) => { + this.#removeElement(elementName); + }); + }; + + #updateCallback = (elements: any[]) => { + elements.forEach((element) => { + if (this.elements[element.elementName]) { + this.elements[element.elementName].updateElementGroup(element); + } + }); + }; + + #hasElementName = (name: string) => { + const tempElements = Object.keys(this.elements); + for (let i = 0; i < tempElements.length; i += 1) { + if (atob(tempElements[i].split(':')[2]) === name) { + return true; + } + } + return false; + }; + + collect = (options: TOptions = {} as TOptions): Promise => { + this.isSkyflowFrameReady = this.metaData.skyflowContainer.isControllerFrameReady; + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (Object.keys(this.elements).length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); + } + this.removeStaleElements(); + const collectElements = Object.values(this.elements); + const elementIds = Object.keys(this.elements) + .map((element) => ({ frameId: element, elementId: element })); + collectElements.forEach((element) => { + if (!element.isMounted()) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + element.isValidElement(); + }); + const resolvedOptions = this.validateCollectOptions(options); + + const emit = () => { + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .emit( + ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.metaData.uuid, + { + type: COLLECT_TYPES.COLLECT, + // Spread the normalized options bag as an index-signature type so + // the framebus payload stays assignable (the emit arg is untyped). + // `tokens` is already resolved inside validateCollectOptions. + ...(resolvedOptions as Record), + elementIds, + containerId: this.containerId, + errorMessages: this.customErrorMessages, + }, + (data: any) => { + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.context.logLevel); + reject(this.wrapCollectError(data?.error)); + } else { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + resolve(data); + } + }, + ); + }; + + if (this.isSkyflowFrameReady) { + emit(); + // EMIT_EVENT logged synchronously in the ready path only, preserving the + // original log timing (the deferred not-ready path never logged it). + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), + MessageType.LOG, this.context.logLevel); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.containerId, emit); + } + } catch (err: any) { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); + }; + + protected removeStaleElements = (): void => { + try { + if (this.#hasNoElements()) return; + + const mountedIframeIds = this.#getMountedIframeIds(); + if (!mountedIframeIds.length) return; + + this.#removeUnmountedElements(mountedIframeIds); + } catch (error: unknown) { + printLog(`${error}`, MessageType.LOG, this.context.logLevel); + } + }; + + #hasNoElements = (): boolean => Object.keys(this.elements).length === 0; + + #getMountedIframeIds = (): string[] => { + const body = document?.body; + if (!body) return []; + + const iframes = body.getElementsByTagName('iframe'); + if (!iframes?.length) return []; + + return Array.from(iframes).map((iframe) => iframe.id); + }; + + #removeUnmountedElements = (mountedIframeIds: string[]): void => { + Object.entries(this.elements).forEach(([key, element]) => { + if (this.#shouldRemoveElement(element, mountedIframeIds)) { + delete this.elements[key]; + } + }); + }; + + #shouldRemoveElement = ( + element: CollectElement, + mountedIframeIds: string[], + ): boolean => ( + element.isMounted() + && !mountedIframeIds.includes(element.iframeName()) + ); + + // ---- Injected divergence (see class doc) -------------------------------- + // create() input validator: privacyDB validates table/skyflowID, flowDB + // validates tableName/skyflowId (each forwards to its own package validator). + protected abstract validateCreateInput(input: TCreateInput): void; + + // The one variant identity field folded into the element descriptor from a + // create() call: privacyDB `{ accept: options.allowedFileType }` (file API), + // flowDB `{ table: input.tableName }` (client tableName → internal table). + protected abstract buildCreateElementFields( + input: TCreateInput, + options: TCreateOptions, + ): Record; + + // Single collect-options seam: validates the options AND resolves the emitted + // `tokens` value, returning a normalized copy (never mutates the caller's + // object). Because `TOptions extends ICollectOptionsBase` (a structural marker), + // the divergent fields are read through a local cast, not the bound. + // Base default = privacyDB: validate a provided `tokens` boolean, validate + // additionalFields/upsert, default tokens to true. flowDB overrides to skip + // token validation and force tokens on while keeping field validation. + // eslint-disable-next-line class-methods-use-this + protected validateCollectOptions(options: TOptions): TOptions { + const opts = options as { tokens?: boolean; additionalFields?: any; upsert?: any[] }; + if (Object.prototype.hasOwnProperty.call(opts, 'tokens') && !validateBooleanOptions(opts.tokens)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); + } + if (opts.additionalFields) { + validateAdditionalFieldsInCollect(opts.additionalFields); + } + if (opts.upsert) { + validateUpsertOptions(opts.upsert); + } + return { ...options, tokens: opts.tokens !== undefined ? opts.tokens : true } as TOptions; + } + + // Error mapping: identity for privacyDB, SkyflowFlowDBError for flowDB. + // eslint-disable-next-line class-methods-use-this + protected wrapCollectError(err: any): any { + return err; + } +} +export default CollectContainer; diff --git a/src/core/external/collect/collect-element.ts b/core/external/collect/collect-element.ts similarity index 89% rename from src/core/external/collect/collect-element.ts rename to core/external/collect/collect-element.ts index 7856b62c7..73e90b655 100644 --- a/src/core/external/collect/collect-element.ts +++ b/core/external/collect/collect-element.ts @@ -1,7 +1,18 @@ /* Copyright (c) 2022 Skyflow, Inc. */ +// Shared collect element (main-thread), lifted to @core (Task 4.7). The two +// packages shipped near-identical copies; the only variant behavior is collect +// key normalization (privacyDB internal `skyflowID`/`table` vs flowDB +// client-facing `skyflowId`/`tableName`), routed through the registered +// VariantAdapter's `collect` surface. `getVariantAdapter()` is read lazily at +// call time; this file never runs in the iframe bundle (main-thread importers +// only: collect/compose containers + index-node), where an adapter is always +// registered. /* eslint-disable no-underscore-dangle */ +import EventEmitter from '@core/event-emitter'; +import Bus from '@core/libs/bus'; +import deepClone from '@core/libs/deep-clone'; import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, @@ -9,43 +20,44 @@ import { EVENT_TYPES, METRIC_TYPES, ELEMENT_TYPES, - ElementType, -} from '../../constants'; -import EventEmitter from '../../../event-emitter'; -import Bus from '../../../libs/bus'; -import deepClone from '../../../libs/deep-clone'; + BaseElementType, + FileElementType, +} from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import properties from '@core/properties'; +import SkyflowElement from '@core/external/common/skyflow-element'; +import SkyflowError from '@core/errors'; +import { + initalizeMetricObject, + pushElementEventWithTimeout, + updateMetricObjectValue, +} from '@core/metrics'; import { formatValidations, getElements, validateAndSetupGroupOptions, -} from '../../../libs/element-options'; -import IFrame from '../common/iframe'; +} from '@core/libs/element-options'; +import IFrame from '@core/external/common/iframe'; import { printLog, getElementName, parameterizedString, EnvOptions, -} from '../../../utils/logs-helper'; -import SkyflowError from '../../../libs/skyflow-error'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import logs from '../../../utils/logs'; -import { - CollectElementUpdateOptions, - Context, Env, EventName, MessageType, -} from '../../../utils/common'; +} from '@core/utils/logs-helper'; import { formatFrameNameToId, getReturnValue, -} from '../../../utils/helpers'; -import SkyflowElement from '../common/skyflow-element'; -import { ContainerType } from '../../../skyflow'; +} from '@core/helpers'; import { - initalizeMetricObject, - pushElementEventWithTimeout, - updateMetricObjectValue, -} from '../../../metrics'; -import { Metadata, ContainerProps, InternalState } from '../../internal/internal-types'; -import properties from '../../../properties'; + ICollectElementUpdateOptionsBase, + Context, Env, EventName, MessageType, + ContainerType, ContainerProps, InternalState, + ICoreMetadata as Metadata, + VariantCollectAdapter, +} from '@core/types'; const CLASS_NAME = 'Element'; -class CollectElement extends SkyflowElement { +class CollectElement< + TUpdateOptions extends ICollectElementUpdateOptionsBase = ICollectElementUpdateOptionsBase, +> extends SkyflowElement { elementType: string; type: string = ContainerType.COLLECT; @@ -96,6 +108,11 @@ class CollectElement extends SkyflowElement { #isUpdateCalled = false; + // Package-specific collect key strategy, injected by the owning CollectContainer + // (privacyDB vs flowDB `skyflowId`/`table` naming). Replaces the former global + // getVariantAdapter().collect lookup on the element's update/validation path. + #collectVariant: VariantCollectAdapter; + constructor( elementId: string, elementGroup: any, @@ -105,10 +122,12 @@ class CollectElement extends SkyflowElement { destroyCallback: Function, updateCallback: Function, context: Context, + collectVariant: VariantCollectAdapter, groupEventEmitter?: EventEmitter, ) { super(); + this.#collectVariant = collectVariant; this.containerId = container.containerId; this.#elementId = elementId; this.#context = context; @@ -252,6 +271,7 @@ class CollectElement extends SkyflowElement { } else if (domElement instanceof HTMLElement) { this.resizeObserver?.observe(domElement); } + const isComposable = this.#elements.length > 1; if (isComposable) { this.#iframe.mount(domElement, this.#elementId, { @@ -359,7 +379,7 @@ class CollectElement extends SkyflowElement { } }; - updateElement = (elementOptions: { elementName: string } & CollectElementUpdateOptions) => { + updateElement = (elementOptions: { elementName: string } & TUpdateOptions) => { this.#bus.emit(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + elementOptions.elementName, { name: elementOptions.elementName, options: elementOptions, @@ -367,8 +387,12 @@ class CollectElement extends SkyflowElement { }); }; - update = (options: CollectElementUpdateOptions) => { + update = (options: TUpdateOptions) => { this.#isUpdateCalled = true; + // Normalize client-facing option keys to the internal names the SET_VALUE + // handler (core/internal/index.ts) consumes. Variant-specific: privacyDB is + // a no-op; flowDB remaps `skyflowId`->`skyflowID` and `tableName`->`table`. + this.#collectVariant.normalizeUpdateOptions(options as Record); if (this.#mounted) { options.validations = formatValidations(options.validations); this.updateElement({ elementName: this.#group.elementName, ...options }); @@ -461,13 +485,13 @@ class CollectElement extends SkyflowElement { data.value = ''; } - if (data.elementType !== ElementType.CARD_NUMBER) delete data.selectedCardScheme; + if (data.elementType !== BaseElementType.CARD_NUMBER) delete data.selectedCardScheme; delete data.isComplete; delete data.name; handler(data); }); if (eventName === ELEMENT_EVENTS_TO_CLIENT.READY) { - this.#bus.emit(ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY, { + this.#bus.emit(ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY + this.iframeName(), { ready: this.#readyToMount, name: this.iframeName(), }); @@ -487,7 +511,7 @@ class CollectElement extends SkyflowElement { }; #registerIFrameBusListener = () => { - this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY, (data) => { + this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY + this.iframeName(), (data) => { if (data.ready && data.name === this.iframeName()) { const { name, ...elementState } = this.#states[0]; this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY, { @@ -539,8 +563,8 @@ class CollectElement extends SkyflowElement { this.#states[index].isFocused = data.value.isFocused; this.#states[index].isRequired = data.value.isRequired; this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; - if (element.elementType === ElementType.MULTI_FILE_INPUT - || element.elementType === ElementType.FILE_INPUT) { + if (element.elementType === FileElementType.MULTI_FILE_INPUT + || element.elementType === FileElementType.FILE_INPUT) { this.#states[index].metaData = data?.value?.metaData || []; } if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; @@ -709,7 +733,10 @@ class CollectElement extends SkyflowElement { if (!(typeof this.#elements[i].column === 'string' || this.#elements[i].column instanceof String)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_COLUMN_IN_COLLECT, [], true); } - if (this.#elements[i].skyflowID !== undefined && !this.#elements[i].skyflowID) { + // The key carrying the skyflow id on an element is variant-specific + // (privacyDB `skyflowID` vs flowDB `skyflowId`). + const skyflowIdKey = this.#collectVariant.skyflowIdKey; + if (this.#elements[i][skyflowIdKey] !== undefined && !this.#elements[i][skyflowIdKey]) { throw new SkyflowError( SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_COLLECT, [], true, ); diff --git a/core/external/collect/composable-collect-container.ts b/core/external/collect/composable-collect-container.ts new file mode 100644 index 000000000..ad150d310 --- /dev/null +++ b/core/external/collect/composable-collect-container.ts @@ -0,0 +1,412 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// Shared composable-collect container base (extends ComposableContainerBase). Owns +// the entire variant-agnostic collect surface: the createMultipleElement shell +// (grid → @core CollectElement, with destroy/update callbacks), the bus +// COMPOSABLE_CONTAINER handshake (registerReadyListener + updateListeners), the +// on() submit listener and collect() — which just emits COMPOSABLE_CALL_REQUESTS +// with COLLECT_TYPES.COLLECT and resolves on the unified { records } envelope. The +// flowDB-vs-privacyDB request/response mapping lives entirely inside the collect +// frame controller, so nothing here branches on variant. create() is single- +// sourced here too (its returned ComposableElement is the shared @core class both +// packages re-export); it is generic over the public input/options types so each +// package keeps its own consumer-facing signature. The divergence stays in each +// package subclass: +// - validateCreateInput() — each package's collect-input validator. +// - buildCreateElementFields() — the one variant identity field on the element +// descriptor (flowDB `table`; privacyDB none). +// - registerElementListeners() — privacyDB's per-element file-upload wiring; +// flowDB has no file upload so it inherits the +// empty ComposableContainerBase default. +// - uploadFiles() — privacyDB-only, added in that subclass. +import bus from 'framebus'; +import deepClone from '@core/libs/deep-clone'; +import uuid from '@core/libs/uuid'; +import properties from '@core/properties'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import { + ELEMENT_EVENTS_TO_IFRAME, + ELEMENTS, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, + COLLECT_TYPES, +} from '@core/constants'; +import ComposableContainerBase from '@core/external/common/composable-container'; +import SkyflowError from '@core/errors'; +import { + getElements, validateElementOptions, formatValidations, formatOptions, +} from '@core/libs/element-options'; +import Client from '@core/client'; +import CollectElement from '@core/external/collect/collect-element'; +import ComposableElement from '@core/external/collect/composable-collect-element'; +import { + ContainerType, MessageType, InputStyles, ErrorTextStyles, + ICollectOptionsBase, + ICollectResponseBase, + CollectElementInput, + ICollectElementOptionsBase, + VariantCollectAdapter, +} from '@core/types'; +import { + validateInitConfig, validateAdditionalFieldsInCollect, validateUpsertOptions, + validateBooleanOptions, +} from '@core/validators'; +import { printLog, parameterizedString } from '@core/utils/logs-helper'; +import { ElementGroup } from '@core/external/collect/collect-container'; + +export interface ComposableElementGroup extends ElementGroup { + styles: InputStyles; + errorTextStyles: ErrorTextStyles; +} + +const CLASS_NAME = 'CollectContainer'; + +abstract class CoreComposableCollectContainer< + TOptions extends ICollectOptionsBase, + TResponse extends ICollectResponseBase, + TCreateInput extends CollectElementInput, + TCreateOptions extends ICollectElementOptionsBase, +> extends ComposableContainerBase { + type:string = ContainerType.COMPOSABLE; + + // Package-specific collect key strategy (privacyDB `skyflowID`/`table` vs flowDB + // `skyflowId`/`tableName`). Abstract — each package MUST supply it (no default, + // so a missing impl is a compile error). Read here (skyflowIdKey) and injected + // into every CollectElement this container builds; mirrors CoreCollectContainer. + protected abstract collectVariant: VariantCollectAdapter; + + protected elementGroup: ComposableElementGroup = { rows: [], styles: {}, errorTextStyles: {} }; + + // eslint-disable-next-line class-methods-use-this + protected getClassName(): string { + return CLASS_NAME; + } + + // Shared create() orchestration. The two packages differed only in the input + // validator and a single identity field on the element descriptor, both now + // injected via hooks (validateCreateInput / buildCreateElementFields), so the + // body is single-sourced here. The returned ComposableElement is the shared + // @core class both packages re-export. Typed over TCreateInput/TCreateOptions + // so each package's public signature keeps its own input/options keys. + create = ( + input: TCreateInput, + options: TCreateOptions = { required: false } as TCreateOptions, + ): ComposableElement => { + this.validateCreateInput(input); + const validations = formatValidations(input.validations); + const formattedOptions = formatOptions(input.type, options, this.context.logLevel); + + const elementName = `${FRAME_ELEMENT}:${input.type}:${btoa(uuid())}`; + + this.elementsList.push({ + elementType: input.type, + name: input.column, + // Hook-provided fields (privacyDB `accept`, flowDB `table`) are spread + // BEFORE `...input` so an explicit input key takes precedence — matching + // the standalone collect path (collect-container.ts) and the 2.7.9 baseline. + ...this.buildCreateElementFields(input, options), + ...input, + ...formattedOptions, + validations, + elementName, + }); + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.tempElements)}:${this.containerId}:${this.context.logLevel}:${btoa(this.clientDomain)}`; + this.iframeID = controllerIframeName; + return new ComposableElement( + elementName, this.eventEmitter, controllerIframeName, + { ...this.metaData, type: input.type }, + ); + }; + + protected createMultipleElement = ( + multipleElements: ComposableElementGroup, + isSingleElementAPI: boolean = false, + ): CollectElement => { + const elements: any[] = []; + this.tempElements = deepClone(multipleElements); + this.tempElements.rows.forEach((row) => { + row.elements.forEach((element) => { + const options = element; + const { elementType } = options; + validateElementOptions(elementType, options); + + options.sensitive = options.sensitive || ELEMENTS[elementType].sensitive; + options.replacePattern = options.replacePattern || ELEMENTS[elementType].replacePattern; + options.mask = options.mask || ELEMENTS[elementType].mask; + + options.isMounted = false; + + options.label = element.label; + // skyflowID (privacyDB) vs skyflowId (flowDB) is a wire-key contract; the + // active key comes from the injected collect variant (was hardcoded to + // `element.skyflowID`, which read `undefined` for flowDB). + options.skyflowID = element[this.collectVariant.skyflowIdKey]; + + elements.push(options); + }); + }); + + this.tempElements.elementName = isSingleElementAPI + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(this.tempElements)}`; + if ( + isSingleElementAPI + && !this.elements[elements[0].elementName] + && this.hasElementName(elements[0].name) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + } + + let element = this.elements[this.tempElements.elementName]; + if (element) { + if (isSingleElementAPI) { + element.update(elements[0]); + } else { + element.update(this.tempElements); + } + } else { + const elementId = uuid(); + element = new CollectElement( + elementId, + this.tempElements, + this.metaData, + { + containerId: this.containerId, + isMounted: this.containerMounted, + type: this.type, + }, + true, + this.destroyCallback, + this.updateCallback, + this.context, + this.collectVariant, + this.eventEmitter, + ); + this.elements[this.tempElements.elementName] = element; + } + return element; + }; + + protected removeElement = (elementName: string) => { + Object.keys(this.elements).forEach((element) => { + if (element === elementName) delete this.elements[element]; + }); + }; + + protected destroyCallback = (elementNames: string[]) => { + elementNames.forEach((elementName) => { + this.removeElement(elementName); + }); + }; + + protected updateCallback = (elements: any[]) => { + elements.forEach((element) => { + if (this.elements[element.elementName]) { + this.elements[element.elementName].update(element); + } + }); + }; + + on = (eventName:string, handler:Function) => { + if (!Object.values(ELEMENT_EVENTS_TO_CLIENT).includes(eventName)) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.INVALID_EVENT_LISTENER, + [], + true, + ); + } + if (!handler) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_HANDLER_IN_EVENT_LISTENER, + [], + true, + ); + } + if (typeof handler !== 'function') { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.INVALID_HANDLER_IN_EVENT_LISTENER, + [], + true, + ); + } + + this.eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.SUBMIT, () => { + handler(); + }); + }; + + // Handshake with the composable controller frame (bus). Invoked from the base + // constructor, so it is a prototype method (available during super()). + protected registerReadyListener(): void { + this.updateListeners(); + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + // The controller-frame emit no longer passes a reply callback (the frame + // builds its client per-request from clientConfig), so guard before calling + // it — otherwise `callback(...)` throws "callback is not a function". + if (typeof callback === 'function') { + callback({ + client: this.metaData.clientJSON, + context: this.context, + }); + } + this.isComposableFrameReady = true; + }); + } + + collect = (options: TOptions = {} as TOptions) : + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (!this.elementsList || this.elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const containerElements = getElements(this.tempElements); + containerElements.forEach((element:any) => { + if (!element?.isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + }); + const elementIds:{ frameId:string, elementId:string }[] = []; + const collectElements = Object.values(this.elements); + collectElements.forEach((element) => { + element.isValidElement(); + }); + const resolvedOptions = this.validateCollectOptions(options); + this.elementsList.forEach((element) => { + elementIds.push({ + frameId: this.tempElements.elementName, + elementId: element.elementName ?? '', + }); + }); + const client = Client.fromJSON(this.metaData.clientJSON) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + this.emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.containerId, { + data: { + type: COLLECT_TYPES.COLLECT, + // Spread the normalized options bag as an index-signature type so the + // payload stays assignable; `tokens` is already resolved inside + // validateCollectOptions. + ...(resolvedOptions as Record), + elementIds, + containerId: this.containerId, + }, + clientConfig: { + vaultURL: this.metaData.clientJSON.config.vaultURL, + vaultID: this.metaData.clientJSON.config.vaultID, + authToken, + }, + errorMessages: this.customErrorMessages, + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + }); + window.addEventListener('message', (event) => { + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.context.logLevel); + reject(this.wrapCollectError(data?.error)); + } else if (data?.records) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.context.logLevel); + reject(data); + } + } + } + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), + MessageType.LOG, this.context.logLevel); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); + + // ---- Injected divergence (see class doc) -------------------------------- + // create() input validator: privacyDB validates table/skyflowID, flowDB + // validates tableName/skyflowId (each forwards to its own package validator). + protected abstract validateCreateInput(input: TCreateInput): void; + + // The one variant identity field folded into the element descriptor from a + // create() call: flowDB `{ table: input.tableName }` (client tableName → + // internal table); privacyDB adds none (returns {}). + protected abstract buildCreateElementFields( + input: TCreateInput, + options: TCreateOptions, + ): Record; + + // Single collect-options seam (mirrors CoreCollectContainer.validateCollectOptions): + // validates the options AND resolves the emitted `tokens` value, returning a + // normalized copy (never mutates the caller's object). Because `TOptions extends + // ICollectOptionsBase` (a structural marker), the divergent fields are read + // through a local cast, not the bound. Base default = privacyDB; flowDB overrides + // to force tokens on while keeping field validation, and maps errors via + // wrapCollectError. + // eslint-disable-next-line class-methods-use-this + protected validateCollectOptions(options: TOptions): TOptions { + const opts = options as { tokens?: boolean; additionalFields?: any; upsert?: any[] }; + if (Object.prototype.hasOwnProperty.call(opts, 'tokens') && !validateBooleanOptions(opts.tokens)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); + } + if (opts.additionalFields) { + validateAdditionalFieldsInCollect(opts.additionalFields); + } + if (opts.upsert) { + validateUpsertOptions(opts.upsert); + } + return { ...options, tokens: opts.tokens !== undefined ? opts.tokens : true } as TOptions; + } + + // Error mapping: identity for privacyDB, SkyflowFlowDBError for flowDB. + // eslint-disable-next-line class-methods-use-this + protected wrapCollectError(err: any): any { + return err; + } + + // Registered from registerReadyListener (prototype method, available during + // super()); relays element-option updates to the mounted controller element. + protected updateListeners(): void { + this.eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS, (data) => { + let elementIndex; + const elementList = this.elementsList.map((element, index) => { + if (element.elementName === data.elementName) { + elementIndex = index; + return { + elementName: element.elementName, + ...data.elementOptions, + }; + } + return element; + }); + + if (this.containerElement) { + this.containerElement.updateElement({ + ...elementList[elementIndex], + }); + } + }); + } +} +export default CoreComposableCollectContainer; diff --git a/src/core/external/collect/compose-collect-element.ts b/core/external/collect/composable-collect-element.ts similarity index 70% rename from src/core/external/collect/compose-collect-element.ts rename to core/external/collect/composable-collect-element.ts index be880d301..ac12f5f54 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/core/external/collect/composable-collect-element.ts @@ -1,18 +1,37 @@ -import { Context } from 'vm'; -import EventEmitter from '../../../event-emitter'; -import { formatValidations } from '../../../libs/element-options'; -import SkyflowError from '../../../libs/skyflow-error'; -import { ContainerType } from '../../../skyflow'; +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// Shared composable collect element (the object returned by ComposableContainer's +// create()). Variant-agnostic: it only relays element-option updates and the +// per-element on()/uploadMultipleFiles() wiring through the event bus, so both +// privacyDB and flowDB use it unchanged. uploadMultipleFiles throws for any +// non-MULTI_FILE_INPUT element, so the flowDB build (no file upload) never +// exercises it. +import EventEmitter from '@core/event-emitter'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import { - CollectElementUpdateOptions, EventName, MessageType, MetaData, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '../../constants'; -import { printLog } from '../../../utils/logs-helper'; -import logs from '../../../utils/logs'; -import properties from '../../../properties'; - -class ComposableElement { + ELEMENT_EVENTS_TO_CLIENT, + ELEMENT_EVENTS_TO_IFRAME, + BaseElementType, + FileElementType, + AnyElementType, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import properties from '@core/properties'; +import SkyflowError from '@core/errors'; +import { formatValidations } from '@core/libs/element-options'; +import { + ICollectElementUpdateOptionsBase, EventName, MessageType, MetaData, ContainerType, Context, +} from '@core/types'; +import { printLog } from '@core/utils/logs-helper'; + +// Generic over the update-options type so each package binds its own identity +// keys (privacyDB `table`/`skyflowID` vs flowDB `tableName`/`skyflowId`) onto +// update(). Defaults to the identity-neutral @core base, so any unparameterized +// use (and the @core internals) are unchanged. +class ComposableElement< + TUpdateOptions extends ICollectElementUpdateOptionsBase = ICollectElementUpdateOptionsBase, +> { #elementName: string; #eventEmitter: EventEmitter; @@ -29,7 +48,7 @@ class ComposableElement { #context: Context; - #elementType: ElementType; + #elementType: AnyElementType; constructor(name, eventEmitter, iframeName, metaData) { this.#elementName = name; @@ -44,7 +63,7 @@ class ComposableElement { logLevel: this.#metaData?.clientJSON?.config?.options?.logLevel, env: this.#metaData?.clientJSON?.config?.options?.env, }; - this.#elementType = this.#metaData?.type as ElementType; + this.#elementType = this.#metaData?.type as AnyElementType; } on(eventName: string, handler: Function) { @@ -75,7 +94,7 @@ class ComposableElement { data.value = ''; } - if (data.elementType !== ElementType.CARD_NUMBER) delete data.selectedCardScheme; + if (data.elementType !== BaseElementType.CARD_NUMBER) delete data.selectedCardScheme; delete data.isComplete; delete data.name; @@ -91,7 +110,7 @@ class ComposableElement { return this.#elementName; } - update = (options: CollectElementUpdateOptions) => { + update = (options: TUpdateOptions) => { this.#isUpdateCalled = true; if (this.#isMounted) { options.validations = formatValidations(options.validations); @@ -119,7 +138,7 @@ class ComposableElement { uploadMultipleFiles = (metaData?: MetaData) => new Promise((resolve, reject) => { try { - if (this.#elementType !== ElementType.MULTI_FILE_INPUT) { + if (this.#elementType !== FileElementType.MULTI_FILE_INPUT) { throw new SkyflowError( SKYFLOW_ERROR_CODE.MULTI_FILE_NOT_SUPPORTED, [], diff --git a/core/external/common/composable-container.ts b/core/external/common/composable-container.ts new file mode 100644 index 000000000..7b65f6568 --- /dev/null +++ b/core/external/common/composable-container.ts @@ -0,0 +1,269 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// Shared base for the composable (single controller-frame) containers. Owns the +// controller-iframe bootstrap, the flex-grid mount() (layout → rows → styles → +// createMultipleElement → shadowRoot/height wiring), hasElementName, unmount and +// the shadowRoot-vs-document postMessage emitEvent. The genuinely divergent +// pieces are injected as hooks so no product-specific symbol is imported into +// @core (the core ⇏ packages boundary): +// - className (getter) — log identity, needed during super() so it is +// a getter, not a field. +// - registerReadyListener() — collect: bus COMPOSABLE_CONTAINER handshake; +// reveal: window `message` MOUNTED flag. Called +// from the constructor, so a prototype method. +// - createMultipleElement — instantiates the product's element class. +// - registerElementListeners() — collect-only per-element file-upload wiring +// (empty default keeps it out of reveal). +// - decorateEmitOptions() — reveal spreads errorMessages into the frame +// payload; collect does not. +// - mountLabel — the element name in the empty-mount error. +// The public create()/collect()/reveal()/uploadFiles()/on() bodies stay in the +// subclasses. +import sum from 'lodash/sum'; +import EventEmitter from '@core/event-emitter'; +import uuid from '@core/libs/uuid'; +import iframer, { setAttributes, getIframeSrc, setStyles } from '@core/iframe-libs/iframer'; +import properties from '@core/properties'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import { + COLLECT_FRAME_CONTROLLER, + CONTROLLER_STYLES, + ELEMENT_EVENTS_TO_CLIENT, +} from '@core/constants'; +import Container from '@core/external/common/container'; +import SkyflowError from '@core/errors'; +import { + Context, MessageType, ContainerOptions, ErrorType, ICoreMetadata, ISkyflowElement, +} from '@core/types'; +import { printLog, parameterizedString } from '@core/utils/logs-helper'; + +abstract class ComposableContainerBase< + TElement extends ISkyflowElement, +> extends Container { + protected containerId: string = ''; + + protected elements: Record = {}; + + protected metaData: ICoreMetadata; + + protected elementGroup: any = { rows: [] }; + + protected elementsList: any = []; + + protected context: Context; + + protected eventEmitter: EventEmitter; + + protected isMounted: boolean = false; + + protected options!: ContainerOptions; + + protected containerElement!: TElement; + + protected containerMounted: boolean = false; + + protected tempElements: any = {}; + + protected clientDomain: string = ''; + + protected isComposableFrameReady: boolean = false; + + protected shadowRoot: ShadowRoot | null = null; + + protected iframeID: string = ''; + + protected getSkyflowBearerToken: () => Promise | undefined; + + protected customErrorMessages: Partial> = {}; + + // Element name in the empty-mount error ('CollectElement' / 'RevealElement'). + protected mountLabel: string = 'CollectElement'; + + // Log identity; overridden per subclass. A concrete overridable method (not an + // abstract getter) so it can be invoked from the constructor without TS2715 — + // virtual dispatch still resolves to the subclass override during super(). + // eslint-disable-next-line class-methods-use-this + protected getClassName(): string { + return 'CollectContainer'; + } + + // The container-create log message; overridden by the composable reveal subclass + // to CREATE_REVEAL_CONTAINER. A concrete overridable method (like getClassName) + // so virtual dispatch resolves to the subclass override during super(). + // eslint-disable-next-line class-methods-use-this + protected getCreateContainerLog(): string { + return logs.infoLogs.CREATE_COLLECT_CONTAINER; + } + + // Instantiates the product's element class; created per subclass because the + // element class + constructor signature diverge (and would cross the boundary). + protected abstract createMultipleElement: ( + multipleElements: any, + isSingleElementAPI?: boolean, + ) => TElement; + + constructor( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ) { + super(); + this.containerId = uuid(); + this.metaData = { + ...metaData, + clientJSON: { + ...metaData?.clientJSON, + config: { + ...metaData?.clientJSON?.config, + options: { + ...metaData?.clientJSON?.config?.options, + ...options, + }, + }, + }, + }; + this.getSkyflowBearerToken = metaData?.getSkyflowBearerToken; + this.context = context; + // Composable containers are only ever created via BaseSkyflow's COMPOSABLE / + // COMPOSE_REVEAL paths, which validate and pass options — so it is present here. + this.options = options as ContainerOptions; + this.eventEmitter = new EventEmitter(); + + this.clientDomain = this.metaData.clientDomain || ''; + const iframe = iframer({ + name: `${COLLECT_FRAME_CONTROLLER}:${this.containerId}:${this.context.logLevel}:${btoa(this.clientDomain)}`, + referrer: this.clientDomain, + }); + setAttributes(iframe, { + src: getIframeSrc(), + }); + setStyles(iframe, { ...CONTROLLER_STYLES }); + printLog(parameterizedString(this.getCreateContainerLog(), this.getClassName()), + MessageType.LOG, + this.context.logLevel); + this.containerMounted = true; + this.registerReadyListener(); + } + + setError(errors: Partial>) { + this.customErrorMessages = errors; + } + + protected hasElementName = (name: string) => { + const tempElements = Object.keys(this.elements); + for (let i = 0; i < tempElements.length; i += 1) { + if (atob(tempElements[i].split(':')[2]) === name) { + return true; + } + } + return false; + }; + + mount = (domElement: HTMLElement | string) => { + if (!domElement) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, + [this.mountLabel], true); + } + + const { layout } = this.options; + if (sum(layout) !== this.elementsList.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, [], true); + } + let count = 0; + layout.forEach((rowCount, index) => { + this.elementGroup.rows = [ + ...this.elementGroup.rows, + { elements: [] }, + ]; + for (let i = 0; i < rowCount; i++) { + this.elementGroup.rows[index].elements.push( + this.elementsList[count], + ); + count++; + } + }); + if (this.options.styles) { + this.elementGroup.styles = { + ...this.options.styles, + }; + } + if (this.options.errorTextStyles) { + this.elementGroup.errorTextStyles = { + ...this.options.errorTextStyles, + }; + } + + if (this.containerMounted) { + this.containerElement = this.createMultipleElement(this.elementGroup, false); + this.containerElement.mount(domElement); + this.isMounted = true; + } + this.registerElementListeners(); + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.shadowRoot !== null) { + this.eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.iframeID, {}); + } + }; + + unmount = () => { + this.containerElement.unmount(); + }; + + protected emitEvent = (eventName: string, options?: Record, callback?: any) => { + const option = this.decorateEmitOptions(options); + if (this.shadowRoot) { + const iframe = this.shadowRoot.getElementById(this.iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...option, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...option, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; + + // ---- Injected divergence (see class doc) -------------------------------- + + // collect: the bus COMPOSABLE_CONTAINER handshake (+ updateListeners); + // reveal: the window `message` MOUNTED readiness flag. Invoked from the + // constructor, so subclasses implement it as a prototype method. + protected abstract registerReadyListener(): void; + + // collect-only per-element file-upload wiring; empty by default so the + // reveal bundle never reaches it. + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + protected registerElementListeners(): void {} + + // reveal spreads its custom error messages into every frame payload; collect + // sends the options through unchanged. + // eslint-disable-next-line class-methods-use-this + protected decorateEmitOptions(options?: Record): Record { + return { + ...options, + }; + } +} +export default ComposableContainerBase; diff --git a/core/external/common/container.ts b/core/external/common/container.ts new file mode 100644 index 000000000..bd109fb65 --- /dev/null +++ b/core/external/common/container.ts @@ -0,0 +1,17 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Shared root of every container (collect / reveal / composable). Not an empty +// marker: it carries the contract all containers implement identically — the +// `type` discriminator and `setError`. State fields (containerId/metaData/context) +// are intentionally left to the subclasses, which diverge in visibility +// (RevealContainer keeps them #private; the others are protected). +import { ErrorType } from '@core/types'; + +abstract class Container { + abstract type: string; + + abstract setError(errors: Partial>): void; +} + +export default Container; diff --git a/src/core/external/common/iframe.ts b/core/external/common/iframe.ts similarity index 77% rename from src/core/external/common/iframe.ts rename to core/external/common/iframe.ts index 96304fbff..ce0b538b2 100644 --- a/src/core/external/common/iframe.ts +++ b/core/external/common/iframe.ts @@ -4,24 +4,23 @@ Copyright (c) 2022 Skyflow, Inc. import iframer, { setAttributes, getIframeSrc, -} from '../../../iframe-libs/iframer'; -import SkyflowError from '../../../libs/skyflow-error'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import { updateMetricObjectValue } from '../../../metrics/index'; -import { METRIC_TYPES } from '../../constants'; -import { LogLevel } from '../../../index-node'; -import { Metadata } from '../../internal/internal-types'; +} from '@core/iframe-libs/iframer'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import { METRIC_TYPES } from '@core/constants'; +import SkyflowError from '@core/errors'; +import { LogLevel, ICoreMetadata } from '@core/types'; +import { updateMetricObjectValue } from '@core/metrics'; export default class IFrame { name: string; - metadata: Metadata; + metadata: ICoreMetadata; iframe: HTMLIFrameElement; container?: Element; - constructor(name: string, metadata: Metadata, containerId: string, logLevel: LogLevel) { + constructor(name: string, metadata: ICoreMetadata, containerId: string, logLevel: LogLevel) { const clientDomain = metadata.clientDomain || ''; this.name = `${name}:${containerId}:${logLevel}:${btoa(clientDomain)}`; this.metadata = metadata; diff --git a/src/core/external/common/skyflow-element.ts b/core/external/common/skyflow-element.ts similarity index 56% rename from src/core/external/common/skyflow-element.ts rename to core/external/common/skyflow-element.ts index 791077e13..1b161a372 100644 --- a/src/core/external/common/skyflow-element.ts +++ b/core/external/common/skyflow-element.ts @@ -1,7 +1,12 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -abstract class SkyflowElement { +import { ISkyflowElement } from '@core/types'; + +// Implements the shared ISkyflowElement contract so the element registry +// threaded through every container can be typed (Record) +// instead of any[]. +abstract class SkyflowElement implements ISkyflowElement { abstract mount(domElementSelector: HTMLElement | string): void; abstract unmount(): void; diff --git a/core/external/reveal/composable-reveal-container.ts b/core/external/reveal/composable-reveal-container.ts new file mode 100644 index 000000000..3fdacb058 --- /dev/null +++ b/core/external/reveal/composable-reveal-container.ts @@ -0,0 +1,348 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// Shared composable-reveal container base (extends ComposableContainerBase). Owns +// the reveal() two-branch orchestration, the createMultipleElement shell, setError +// (with the custom-error-message broadcast), the window `message` MOUNTED +// readiness listener, and the errorMessages-decorated emitEvent. The divergence is +// injected as hooks so no package symbol enters @core: +// - instantiateInternalElement — the product's (renderFile-capable) element. +// - validateRecords — each package's reveal-record validator. +// - validateOptions — flowDB validates reveal options; privacyDB no-op. +// - revealExtraData — flowDB forwards options into the frame payload. +// - handleRevealResponse — privacyDB rejects { errors } raw; flowDB maps a +// { error } full-failure to SkyflowFlowDBError. +// create() stays in each package subclass — its typed input and the returned +// (renderFile-bearing) element are part of the public API surface. +import properties from '@core/properties'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import uuid from '@core/libs/uuid'; +import deepClone from '@core/libs/deep-clone'; +import { + ELEMENT_EVENTS_TO_IFRAME, + FRAME_ELEMENT, + COMPOSABLE_REVEAL, + ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, + CUSTOM_ERROR_MESSAGES, +} from '@core/constants'; +import ComposableContainerBase from '@core/external/common/composable-container'; +import ComposableRevealInternalElement from '@core/external/reveal/composable-reveal-internal'; +import SkyflowError from '@core/errors'; +import { + ContainerType, MessageType, ErrorType, + IRevealResponseBase, +} from '@core/types'; +import { printLog, parameterizedString } from '@core/utils/logs-helper'; +import { formatRevealElementOptions } from '@core/helpers'; +import { validateInitConfig, validateInputFormatOptions } from '@core/validators'; + +abstract class CoreComposableRevealContainer< + TRevealOptions, + TResponse extends IRevealResponseBase, +> extends ComposableContainerBase> { + type:string = ContainerType.COMPOSE_REVEAL; + + protected revealRecords: any[] = []; + + protected mountLabel: string = 'RevealElement'; + + // eslint-disable-next-line class-methods-use-this + protected getClassName(): string { + return 'ComposableRevealContainer'; + } + + // eslint-disable-next-line class-methods-use-this + protected getCreateContainerLog(): string { + return logs.infoLogs.CREATE_REVEAL_CONTAINER; + } + + setError(errors: Partial>) { + this.customErrorMessages = errors; + // eslint-disable-next-line no-underscore-dangle + (this.eventEmitter as any)._emit(`${CUSTOM_ERROR_MESSAGES}:${this.containerId}`, { + errorMessages: this.customErrorMessages, + }); + } + + protected createMultipleElement = ( + multipleElements: any, + isSingleElementAPI: boolean = false, + ): ComposableRevealInternalElement => { + try { + const elements: any[] = []; + this.tempElements = deepClone(multipleElements); + this.tempElements?.rows?.forEach((row) => { + row?.elements?.forEach((element) => { + const options = element ?? {}; + const { elementType } = options; + options.isMounted = false; + + options.label = element?.label; + options.skyflowID = element?.skyflowID; + + elements.push(options); + }); + }); + + this.tempElements.elementName = isSingleElementAPI + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(this.tempElements)}`; + if ( + isSingleElementAPI + && !this.elements[elements[0].elementName] + && this.hasElementName(elements[0].name) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + } + + let element = this.elements[this.tempElements.elementName]; + if (element) { + if (isSingleElementAPI) { + // element.update(elements[0]); + } else { + // element.update(this.tempElements); + } + } else { + const elementId = uuid(); + try { + element = this.instantiateInternalElement(elementId, this.tempElements); + this.elements[this.tempElements.elementName] = element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.context.logLevel); + throw error; + } + } + this.iframeID = element.iframeName(); + return element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.context.logLevel); + throw error; + } + }; + + protected decorateEmitOptions(options?: Record): Record { + return { + ...options, + errorMessages: this.customErrorMessages, + }; + } + + protected registerReadyListener(): void { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.containerId) { + this.isComposableFrameReady = true; + } + }); + } + + reveal(options?: TRevealOptions): Promise { + this.revealRecords = []; + if (this.isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (!this.elementsList || this.elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, this.getClassName()), + MessageType.LOG, + this.context.logLevel); + this.elementsList.forEach((currentElement) => { + if (!currentElement.skyflowID) { + this.revealRecords.push(currentElement); + } + }); + this.validateRecords(this.revealRecords); + this.validateOptions(options); + const elementIds:{ frameId:string, token:string }[] = []; + this.elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, this.getClassName()), + MessageType.LOG, + this.context.logLevel); + this.emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.containerId, + { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.containerId, + elementIds, + ...this.revealExtraData(options), + }, + clientConfig: { + vaultURL: this.metaData?.clientJSON?.config?.vaultURL, + vaultID: this.metaData?.clientJSON?.config?.vaultID, + authToken, + }, + context: this.context, + }, + ); + + window?.addEventListener('message', (event) => { + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId) { + this.handleRevealResponse(event?.data?.data, resolve, reject); + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (!this.elementsList || this.elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, this.getClassName()), + MessageType.LOG, + this.context.logLevel); + this.elementsList.forEach((currentElement) => { + if (!currentElement.skyflowID) { + this.revealRecords.push(currentElement); + } + }); + this.validateRecords(this.revealRecords); + this.validateOptions(options); + const elementIds:{ frameId:string, token:string }[] = []; + this.elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, this.getClassName()), + MessageType.LOG, + this.context.logLevel); + window.addEventListener('message', (messagEevent) => { + if (messagEevent?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (messagEevent?.data?.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.containerId) { + this.emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.containerId, { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.containerId, + elementIds, + ...this.revealExtraData(options), + }, + clientConfig: { + vaultURL: this.metaData.clientJSON.config.vaultURL, + vaultID: this.metaData.clientJSON.config.vaultID, + authToken, + }, + context: this.context, + }, + ); + window.addEventListener('message', (event) => { + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId) { + this.handleRevealResponse(event?.data?.data, resolve, reject); + } + } + }); + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); + } + + // Shared create() body: registers a composable reveal element (uuid, format- + // option validation, elementsList push, controllerIframeName) and returns the + // ids the package subclass needs. create() itself stays per-package because its + // typed input and the returned (renderFile-bearing) element are public API. + protected buildComposableRevealElement( + input: any, + options?: Record, + ): { elementName: string; controllerIframeName: string } { + const elementId = uuid(); + validateInputFormatOptions(options); + + const elementName = `${COMPOSABLE_REVEAL}:${btoa(elementId)}`; + this.elementsList?.push({ + name: elementName, + ...input, + elementName, + elementId, + ...formatRevealElementOptions(options ?? {}), + }); + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.tempElements ?? {})}:${this.containerId}:${this.context?.logLevel}:${btoa(this.clientDomain ?? '')}`; + return { elementName, controllerIframeName }; + } + + // ---- Injected divergence (see class doc) -------------------------------- + + // Instantiates the product's composable reveal internal element (privacyDB's + // carries renderFile; flowDB's is a token-only shim) — kept out of @core. + protected abstract instantiateInternalElement(elementId: string, tempElements: any): any; + + // Each package's reveal-record validator (privacyDB validates skyflowID/table/ + // column/redaction; flowDB is token-only) — divergent, so package-injected. + protected abstract validateRecords(records: any[]): void; + + // flowDB validates reveal options; privacyDB has none (no-op default). + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + protected validateOptions(options?: TRevealOptions): void {} + + // flowDB forwards reveal options into the frame payload; privacyDB omits them. + // eslint-disable-next-line class-methods-use-this + protected revealExtraData(options?: TRevealOptions): Record { + return {}; + } + + // privacyDB default: a partial failure ({ errors }) rejects raw. flowDB + // overrides to map a full failure ({ error }) onto SkyflowFlowDBError. + protected handleRevealResponse( + revealData: any, + resolve: (value: any) => void, + reject: (reason?: any) => void, + ): void { + if (revealData?.errors) { + printLog( + parameterizedString(logs?.errorLogs?.FAILED_REVEAL), + MessageType.ERROR, + this.context?.logLevel, + ); + reject(revealData); + } else { + printLog( + parameterizedString(logs?.infoLogs?.REVEAL_SUBMIT_SUCCESS, this.getClassName()), + MessageType.LOG, + this.context?.logLevel, + ); + resolve(revealData); + } + } +} +export default CoreComposableRevealContainer; diff --git a/core/external/reveal/composable-reveal-element.ts b/core/external/reveal/composable-reveal-element.ts new file mode 100644 index 000000000..67ecc6de5 --- /dev/null +++ b/core/external/reveal/composable-reveal-element.ts @@ -0,0 +1,53 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Shared composable reveal-element base. Holds the mount-tracking + update +// plumbing common to both SDKs. Generic over the reveal-input shape (`TInput`) +// because that shape genuinely diverges (privacyDB: skyflowID/table/column/…; +// flowDB: token-only), so each package binds the generic in a thin subclass. +// privacyDB additionally adds `renderFile`; flowDB adds nothing. +import EventEmitter from '@core/event-emitter'; +import { ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_OPTIONS_TYPES } from '@core/constants'; +import { ContainerType, EventName, IRevealElementOptions } from '@core/types'; + +class ComposableRevealElement { + protected elementName: string; + + protected eventEmitter: EventEmitter; + + #iframeName: string; + + type: string = ContainerType.COMPOSABLE; + + #isMounted: boolean = false; + + constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { + this.elementName = name; + this.#iframeName = iframeName; + this.eventEmitter = eventEmitter; + this.eventEmitter?.on?.(`${EventName.READY}:${this.elementName}`, () => { + this.#isMounted = true; + }); + } + + iframeName(): string { + return this.#iframeName ?? ''; + } + + getID(): string { + return this.elementName ?? ''; + } + + update = (options: TInput | IRevealElementOptions) => { + // eslint-disable-next-line no-underscore-dangle + this.eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${this.elementName}`, + { + options: options as TInput | IRevealElementOptions, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + }, + ); + }; +} + +export default ComposableRevealElement; diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/core/external/reveal/composable-reveal-internal.ts similarity index 51% rename from src/core/external/reveal/composable-reveal-internal.ts rename to core/external/reveal/composable-reveal-internal.ts index 6afdae9f9..54fddbea1 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/core/external/reveal/composable-reveal-internal.ts @@ -2,12 +2,9 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import SkyflowError from '../../../libs/skyflow-error'; -import uuid from '../../../libs/uuid'; -import { - Context, ErrorType, MessageType, RenderFileResponse, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import uuid from '@core/libs/uuid'; +import EventEmitter from '@core/event-emitter'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import { ELEMENT_EVENTS_TO_IFRAME, ELEMENT_EVENTS_TO_CONTAINER, @@ -15,46 +12,42 @@ import { METRIC_TYPES, ELEMENT_EVENTS_TO_CLIENT, EVENT_TYPES, - REVEAL_TYPES, COMPOSABLE_REVEAL, CUSTOM_ERROR_MESSAGES, -} from '../../constants'; -import IFrame from '../common/iframe'; -import SkyflowElement from '../common/skyflow-element'; -import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +} from '@core/constants'; +import properties from '@core/properties'; +import SkyflowElement from '@core/external/common/skyflow-element'; +import SkyflowError from '@core/errors'; import { pushElementEventWithTimeout, updateMetricObjectValue, -} from '../../../metrics'; -import logs from '../../../utils/logs'; -import { parameterizedString, printLog } from '../../../utils/logs-helper'; -import properties from '../../../properties'; -import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; -import EventEmitter from '../../../event-emitter'; -import { formatRevealElementOptions } from '../../../utils/helpers'; -import { Metadata, RevealContainerProps } from '../../internal/internal-types'; - -const CLASS_NAME = 'RevealElementInteranalElement'; - -export interface RevealComposableGroup{ - record: IRevealElementInput - options: IRevealElementOptions -} - -class ComposableRevealInternalElement extends SkyflowElement { +} from '@core/metrics'; +import IFrame from '@core/external/common/iframe'; +import { + Context, ErrorType, IRevealElementOptions, ICoreMetadata, RevealContainerProps, +} from '@core/types'; +import { formatRevealElementOptions } from '@core/helpers'; + +// Shared composable reveal-internal element base. Holds the iframe/mount/update/ +// altText DOM+bus machinery common to both SDKs. Generic over the reveal-input +// shape (`TInput`). The privacyDB file-render feature (`renderFile` + +// `#getSkyflowBearerToken`, which pull in package-only render transport) lives in +// the skyflow-js subclass and is wired via the `registerRenderFileRequestListener` +// hook; token-only flowDB leaves that hook empty and adds nothing. +class ComposableRevealInternalElement extends SkyflowElement { #iframe: IFrame; - #metaData: Metadata; + protected metaData: ICoreMetadata; #recordData: any; - #containerId: string; + protected containerId: string; #isMounted:boolean = false; #isClientSetError:boolean = false; - #context: Context; + protected context: Context; #elementId: string; @@ -62,40 +55,37 @@ class ComposableRevealInternalElement extends SkyflowElement { #readyToMount: boolean = false; - #eventEmitter: EventEmitter; + protected eventEmitter: EventEmitter; #shadowRoot: ShadowRoot | null = null; - #getSkyflowBearerToken: () => Promise | undefined; - - #isComposableFrameReady: boolean = false; + protected isComposableFrameReady: boolean = false; #customerErrorMessages: Partial> = {}; constructor(elementId: string, recordGroup, - metaData: Metadata, + metaData: ICoreMetadata, container: RevealContainerProps, context: Context) { super(); this.#elementId = elementId; - this.#metaData = metaData; + this.metaData = metaData; this.resizeObserver = null; this.#recordData = recordGroup; - this.#containerId = container?.containerId; + this.containerId = container?.containerId; this.#readyToMount = container?.isMounted ?? true; - this.#eventEmitter = container?.eventEmitter; - this.#context = context; + this.eventEmitter = container?.eventEmitter; + this.context = context; this.#iframe = new IFrame( `${COMPOSABLE_REVEAL}:${btoa(uuid())}`, metaData, - this.#containerId, - this.#context?.logLevel, + this.containerId, + this.context?.logLevel, ); this.#readyToMount = true; - this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe?.name, (data) => { this.#iframe?.setIframeHeight(data?.height); @@ -106,7 +96,7 @@ class ComposableRevealInternalElement extends SkyflowElement { this.#iframe?.setIframeHeight(event?.data?.data?.height); } }); - this.#eventEmitter.on(`${CUSTOM_ERROR_MESSAGES}:${this.#containerId}`, (data) => { + this.eventEmitter.on(`${CUSTOM_ERROR_MESSAGES}:${this.containerId}`, (data) => { if (data?.errorMessages) { this.#customerErrorMessages = data.errorMessages as Record; } @@ -130,20 +120,11 @@ class ComposableRevealInternalElement extends SkyflowElement { window?.addEventListener('message', (event) => { if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + element?.name) { - this.#isComposableFrameReady = true; + this.isComposableFrameReady = true; } }); - this.#eventEmitter?.on( - `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, - (data, callback) => { - this.renderFile(element)?.then((response) => { - callback?.(response); - })?.catch((error) => { - callback?.({ error }); - }); - }, - ); - this.#eventEmitter?.on( + this.registerRenderFileRequestListener(element); + this.eventEmitter?.on( `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${element?.name}`, (data) => { if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { @@ -208,8 +189,8 @@ class ComposableRevealInternalElement extends SkyflowElement { updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); if ( - this.#metaData?.clientJSON?.config?.options?.trackMetrics - && this.#metaData.clientJSON.config?.options?.trackingKey + this.metaData?.clientJSON?.config?.options?.trackMetrics + && this.metaData.clientJSON.config?.options?.trackingKey ) { pushElementEventWithTimeout(this.#elementId); } @@ -227,10 +208,10 @@ class ComposableRevealInternalElement extends SkyflowElement { if (this.#readyToMount) { this.#iframe.mount(domElementSelector, undefined, { record: JSON.stringify({ - ...this.#metaData, + ...this.metaData, record: this.#recordData, - context: this.#context, - containerId: this.#containerId, + context: this.context, + containerId: this.containerId, }), }); bus @@ -241,10 +222,10 @@ class ComposableRevealInternalElement extends SkyflowElement { bus // .target(location.origin) .emit( - ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.containerId, { skyflowID: this.#recordData.skyflowID, - containerId: this.#containerId, + containerId: this.containerId, }, ); updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); @@ -253,10 +234,10 @@ class ComposableRevealInternalElement extends SkyflowElement { bus // .target(location.origin) .emit( - ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.containerId, { id: this.#recordData.token, - containerId: this.#containerId, + containerId: this.containerId, }, ); updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); @@ -283,7 +264,7 @@ class ComposableRevealInternalElement extends SkyflowElement { } } - #emitEvent = (eventName: string, options?: Record) => { + protected emitEvent = (eventName: string, options?: Record) => { const option = { ...options, errorMessages: this.#customerErrorMessages, @@ -303,158 +284,17 @@ class ComposableRevealInternalElement extends SkyflowElement { } }; - renderFile(recordData: any): Promise { - let altText = ''; - if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { - altText = recordData.altText; - } - this.setAltText('loading...', recordData); - const loglevel = this.#context.logLevel; - if (this.#isComposableFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), - MessageType.LOG, - loglevel); - validateRenderElementRecord(recordData); - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#emitEvent( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, - { - data: { - type: REVEAL_TYPES.RENDER_FILE, - containerId: this.#containerId, - iframeName: recordData.name, - }, - clientConfig: { - vaultURL: this.#metaData.clientJSON.config.vaultURL, - vaultID: this.#metaData.clientJSON.config.vaultID, - authToken, - }, - }, - ); - window?.addEventListener('message', (event) => { - if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (event?.data - && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE - + recordData.name) { - if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { - const revealData = event?.data?.data?.result; - if (revealData?.error || revealData?.errors) { - printLog(parameterizedString( - logs.errorLogs.FAILED_RENDER, - ), MessageType.ERROR, - this.#context.logLevel); - if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { - this.setAltText(altText, recordData); - } - reject(revealData?.error || revealData?.errors); - } else { - printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, - CLASS_NAME, recordData.skyflowID), - MessageType.LOG, this.#context.logLevel); - resolve(revealData); - } - } - } - } - }); - }).catch((err:any) => { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), - MessageType.LOG, loglevel); - } catch (err: any) { - printLog(`Error: ${err.message}`, MessageType.ERROR, - loglevel); - reject(err); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), - MessageType.LOG, - loglevel); - validateRenderElementRecord(recordData); - window.addEventListener('message', (event) => { - if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + recordData?.name) { - this.#isMounted = true; - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#emitEvent( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, - { - data: { - type: REVEAL_TYPES.RENDER_FILE, - containerId: this.#containerId, - iframeName: recordData.name, - }, - clientConfig: { - vaultURL: this.#metaData.clientJSON.config.vaultURL, - vaultID: this.#metaData.clientJSON.config.vaultID, - authToken, - }, - }, - ); - window.addEventListener('message', (event1) => { - if (event1?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (event1?.data - && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE - + recordData.name) { - if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { - const revealData = event1?.data?.data?.result; - if (revealData?.error || revealData?.errors) { - printLog(parameterizedString( - logs.errorLogs.FAILED_RENDER, - ), MessageType.ERROR, - this.#context.logLevel); - if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { - this.setAltText(altText, recordData); - } - reject(revealData?.error || revealData?.errors); - } else { - // eslint-disable-next-line max-len - printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, - CLASS_NAME, recordData.skyflowID), - MessageType.LOG, this.#context.logLevel); - resolve(revealData); - } - } - } - } - }); - }).catch((err:any) => { - printLog(`${err?.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - }); - } - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), - MessageType.LOG, loglevel); - } catch (err: any) { - printLog(`Error: ${err?.message}`, MessageType.ERROR, - loglevel); - reject(err); - } - }); + // Hook: privacyDB registers a RENDER_FILE_REQUEST listener (which invokes + // renderFile) for the given composable element here. Token-only flowDB leaves + // it empty, so the file-render path never wires up. + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + protected registerRenderFileRequestListener(element: any): void {} + + // Lets the file-render subclass flip the shared mounted flag when the + // composable frame reports RENDER_MOUNTED (the `#isMounted` field is private + // and its name is taken by the public isMounted() method). + protected markMounted(): void { + this.#isMounted = true; } iframeName(): string { @@ -489,8 +329,8 @@ class ComposableRevealInternalElement extends SkyflowElement { } setAltText(altText:string, record) { - if (this.#isComposableFrameReady) { - this.#emitEvent( + if (this.isComposableFrameReady) { + this.emitEvent( ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, @@ -502,7 +342,7 @@ class ComposableRevealInternalElement extends SkyflowElement { window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + record?.name) { - this.#emitEvent( + this.emitEvent( ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record?.name, @@ -529,9 +369,9 @@ class ComposableRevealInternalElement extends SkyflowElement { this.#iframe.unmount(); } - update(options: IRevealElementInput | IRevealElementOptions, record) { - if (this.#isComposableFrameReady) { - this.#emitEvent( + update(options: TInput | IRevealElementOptions, record) { + if (this.isComposableFrameReady) { + this.emitEvent( ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, @@ -543,7 +383,7 @@ class ComposableRevealInternalElement extends SkyflowElement { window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + record?.name) { - this.#emitEvent( + this.emitEvent( ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, { name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, diff --git a/src/core/external/reveal/reveal-container.ts b/core/external/reveal/reveal-container.ts similarity index 66% rename from src/core/external/reveal/reveal-container.ts rename to core/external/reveal/reveal-container.ts index b17008b8f..0283186fc 100644 --- a/src/core/external/reveal/reveal-container.ts +++ b/core/external/reveal/reveal-container.ts @@ -2,58 +2,50 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import EventEmitter from '../../../event-emitter'; -import iframer, { getIframeSrc, setAttributes, setStyles } from '../../../iframe-libs/iframer'; -import SkyflowError from '../../../libs/skyflow-error'; -import uuid from '../../../libs/uuid'; -import { ContainerType } from '../../../skyflow'; -import { - ContainerOptions, - Context, ErrorType, MessageType, - RedactionType, RevealResponse, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import logs from '../../../utils/logs'; -import { parameterizedString, printLog } from '../../../utils/logs-helper'; -import { validateInitConfig, validateInputFormatOptions, validateRevealElementRecords } from '../../../utils/validators'; +import EventEmitter from '@core/event-emitter'; +import uuid from '@core/libs/uuid'; +import iframer, { getIframeSrc, setAttributes, setStyles } from '@core/iframe-libs/iframer'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; import { CONTROLLER_STYLES, CUSTOM_ERROR_MESSAGES, ELEMENT_EVENTS_TO_CONTAINER, ELEMENT_EVENTS_TO_IFRAME, REVEAL_FRAME_CONTROLLER, REVEAL_TYPES, -} from '../../constants'; -import Container from '../common/container'; -import RevealElement from './reveal-element'; -import properties from '../../../properties'; -import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; - -export interface IRevealElementInput { - token?: string; - skyflowID?: string; - table?: string; - column?: string; - redaction?: RedactionType; - inputStyles?: object; - label?: string; - labelStyles?: object; - altText?: string; - errorTextStyles?: object; -} - -export interface IRevealElementOptions { - enableCopy?: boolean; - format?: string; - translation?:Record -} +} from '@core/constants'; +import properties from '@core/properties'; +import { + ContainerType, IRevealElementOptions, ContainerOptions, Context, ErrorType, + MessageType, IRevealResponseBase, ICoreMetadata, RevealContainerProps, +} from '@core/types'; +import Container from '@core/external/common/container'; +import SkyflowError from '@core/errors'; +import { validateInitConfig, validateInputFormatOptions } from '@core/validators'; +import { parameterizedString, printLog } from '@core/utils/logs-helper'; +import CoreRevealElement from '@core/external/reveal/reveal-element'; const CLASS_NAME = 'RevealContainer'; -class RevealContainer extends Container { - #revealRecords: IRevealElementInput[] = []; +// Shared reveal-container base. Owns the controller-frame bootstrap and the +// mount/reveal orchestration common to both SDKs. Generic over the reveal-input +// shape (TInput), the reveal() options (TRevealOptions), and the concrete +// element type (TElement). Divergence is injected via hooks — validateOptions +// (reveal options), validateRecords (reveal-record validation), wrapRevealError +// (error mapping) — and the createRevealElement factory (each package's own +// RevealElement, kept out of @core). The reveal input/option TYPE definitions +// stay in each package's reveal-container subclass (imported by the element +// files as `./reveal-container`). +abstract class RevealContainer< + TInput extends object, + TRevealOptions, + TResponse extends IRevealResponseBase, + TElement extends CoreRevealElement, +> extends Container { + #revealRecords: TInput[] = []; - #revealElements: RevealElement[] = []; + #revealElements: TElement[] = []; #mountedRecords: { id: string }[] = []; - #metaData: Metadata; + #metaData: ICoreMetadata; #containerId: string; @@ -65,8 +57,6 @@ class RevealContainer extends Container { #context: Context; - #skyflowElements: Array; - #isMounted: boolean = false; type:string = ContainerType.REVEAL; @@ -76,8 +66,7 @@ class RevealContainer extends Container { #customErrorMessages: Partial> = {}; constructor( - metaData: Metadata, - skyflowElements: Array, + metaData: ICoreMetadata, context: Context, options?: ContainerOptions, ) { @@ -96,7 +85,6 @@ class RevealContainer extends Container { }, }, }; - this.#skyflowElements = skyflowElements; this.#containerId = uuid(); this.#eventEmmiter = new EventEmitter(); this.#context = context; @@ -144,19 +132,20 @@ class RevealContainer extends Container { ); } - create(record: IRevealElementInput, options?: IRevealElementOptions) { + create(record: TInput, options?: IRevealElementOptions): TElement { // this.#revealRecords.push(record); const elementId = uuid(); validateInputFormatOptions(options); - const revealElement = new RevealElement(record, options, this.#metaData, + const revealElement = this.createRevealElement( + record, options, this.#metaData, { containerId: this.#containerId, isMounted: this.#isMounted, eventEmitter: this.#eventEmmiter, type: ContainerType.REVEAL, - }, elementId, this.#context); + }, elementId, this.#context, + ); this.#revealElements.push(revealElement); - this.#skyflowElements[elementId] = revealElement; return revealElement; } @@ -168,7 +157,7 @@ class RevealContainer extends Container { }); } - reveal(): Promise { + reveal(options?: TRevealOptions): Promise { this.#isRevealCalled = true; this.#revealRecords = []; if (this.#metaData.skyflowContainer.isControllerFrameReady) { @@ -189,7 +178,8 @@ class RevealContainer extends Container { if (this.#revealRecords.length === 0) { throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_REVEAL, [], true); } - validateRevealElementRecords(this.#revealRecords); + this.validateRecords(this.#revealRecords); + this.validateOptions(options); if (!this.#isElementsMounted) { const timeout = setTimeout(() => { printLog(logs.errorLogs.ELEMENTS_NOT_MOUNTED_REVEAL, @@ -201,11 +191,11 @@ class RevealContainer extends Container { ELEMENT_EVENTS_TO_CONTAINER.ALL_ELEMENTS_MOUNTED + this.#containerId, () => { clearTimeout(timeout); - this.#emitRevealRequest(resolve, reject); + this.#emitRevealRequest(resolve, reject, options); }, ); } else { - this.#emitRevealRequest(resolve, reject); + this.#emitRevealRequest(resolve, reject, options); } } catch (err: any) { printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); @@ -230,7 +220,8 @@ class RevealContainer extends Container { if (this.#revealRecords.length === 0) { throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_REVEAL, [], true); } - validateRevealElementRecords(this.#revealRecords); + this.validateRecords(this.#revealRecords); + this.validateOptions(options); if (!this.#isElementsMounted) { const timeout = setTimeout(() => { printLog(logs.errorLogs.ELEMENTS_NOT_MOUNTED_REVEAL, @@ -243,13 +234,13 @@ class RevealContainer extends Container { () => { clearTimeout(timeout); if (this.#metaData.skyflowContainer.isControllerFrameReady) { - this.#emitRevealRequest(resolve, reject); + this.#emitRevealRequest(resolve, reject, options); } else { bus .target(properties.IFRAME_SECURE_ORIGIN) .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#metaData.uuid, () => { - this.#emitRevealRequest(resolve, reject); + this.#emitRevealRequest(resolve, reject, options); }); } }, @@ -259,7 +250,7 @@ class RevealContainer extends Container { .target(properties.IFRAME_SECURE_ORIGIN) .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#metaData.uuid, () => { - this.#emitRevealRequest(resolve, reject); + this.#emitRevealRequest(resolve, reject, options); }); } } catch (err: any) { @@ -269,7 +260,7 @@ class RevealContainer extends Container { }); } - #emitRevealRequest(resolve, reject) { + #emitRevealRequest(resolve, reject, options?: TRevealOptions) { bus .target(properties.IFRAME_SECURE_ORIGIN) .emit( @@ -279,13 +270,17 @@ class RevealContainer extends Container { records: this.#revealRecords, containerId: this.#containerId, errorMessages: this.#customErrorMessages, + // Only include `options` when present. flowDB passes reveal options here; + // privacyDB's value is always undefined and fetchRevealRecords ignores it, + // so the key is omitted rather than sent as `undefined`. + ...(options ? { options } : {}), }, (revealData: any) => { this.#mountedRecords = []; if (revealData.error) { printLog(parameterizedString(logs.errorLogs.FAILED_REVEAL), MessageType.ERROR, this.#context.logLevel); - reject(revealData.error); + reject(this.wrapRevealError(revealData.error)); } else { printLog(parameterizedString(logs.infoLogs.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), MessageType.LOG, @@ -295,5 +290,31 @@ class RevealContainer extends Container { }, ); } + + // ---- Injected divergence (see class doc) -------------------------------- + // Factory: each package builds its own RevealElement (kept out of @core). + protected abstract createRevealElement( + record: TInput, + options: IRevealElementOptions | undefined, + metaData: ICoreMetadata, + container: RevealContainerProps, + elementId: string, + context: Context, + ): TElement; + + // Reveal-record validation (privacyDB allows skyflowID/redaction/format; + // flowDB is token-only), so each package supplies its validator. + protected abstract validateRecords(records: TInput[]): void; + + // Reveal-options validation: no-op for privacyDB (no reveal options), + // validateRevealOptions for flowDB. + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + protected validateOptions(options?: TRevealOptions): void {} + + // Error mapping: identity for privacyDB, SkyflowFlowDBError for flowDB. + // eslint-disable-next-line class-methods-use-this + protected wrapRevealError(err: any): any { + return err; + } } export default RevealContainer; diff --git a/core/external/reveal/reveal-element.ts b/core/external/reveal/reveal-element.ts new file mode 100644 index 000000000..c7b95a365 --- /dev/null +++ b/core/external/reveal/reveal-element.ts @@ -0,0 +1,364 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import bus from 'framebus'; +import uuid from '@core/libs/uuid'; +import EventEmitter from '@core/event-emitter'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import { + // eslint-disable-next-line max-len + FRAME_REVEAL, + ELEMENT_EVENTS_TO_IFRAME, + ELEMENT_EVENTS_TO_CONTAINER, + REVEAL_ELEMENT_OPTIONS_TYPES, + METRIC_TYPES, + ELEMENT_EVENTS_TO_CLIENT, + ELEMENT_TYPES, + EVENT_TYPES, + CUSTOM_ERROR_MESSAGES, +} from '@core/constants'; +import properties from '@core/properties'; +import SkyflowElement from '@core/external/common/skyflow-element'; +import SkyflowError from '@core/errors'; +import { + initalizeMetricObject, + pushElementEventWithTimeout, + updateMetricObjectValue, +} from '@core/metrics'; +import IFrame from '@core/external/common/iframe'; +import { + Context, ErrorType, IRevealElementOptions, ICoreMetadata, RevealContainerProps, +} from '@core/types'; +import { formatRevealElementOptions } from '@core/helpers'; + +// Shared reveal-element base. Holds the iframe/mount/update/error DOM+bus +// machinery common to both SDKs. Generic over the reveal-input shape (`TInput`) +// since that diverges per package. The privacyDB file-render feature +// (`renderFile`, which pulls in package-only reveal transport) lives in the +// skyflow-js subclass; flowDB binds the generic and adds nothing. +class RevealElement extends SkyflowElement { + protected iframe: IFrame; + + protected metaData: ICoreMetadata; + + protected recordData: any; + + protected containerId: string; + + #isMounted:boolean = false; + + #isClientSetError:boolean = false; + + protected context: Context; + + #elementId: string; + + #readyToMount: boolean = false; + + #eventEmitter: EventEmitter; + + #isFrameReady: boolean; + + #domSelecter: string; + + #clientId: string; + + protected customerErrorMessages: Partial> = {}; + + constructor( + record: TInput, + options: IRevealElementOptions = {}, + metaData: ICoreMetadata, + container: RevealContainerProps, + elementId: string, + context: Context, + ) { + super(); + this.#elementId = elementId; + this.metaData = metaData; + this.#clientId = this.metaData.uuid; + this.recordData = { + ...record, + ...formatRevealElementOptions(options), + }; + this.containerId = container.containerId; + this.#readyToMount = container.isMounted; + this.#eventEmitter = container.eventEmitter; + this.context = context; + initalizeMetricObject(metaData, elementId); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.ELEMENT_TYPE_KEY, ELEMENT_TYPES.REVEAL); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.CONTAINER_NAME, ELEMENT_TYPES.REVEAL); + this.iframe = new IFrame( + `${FRAME_REVEAL}:${btoa(uuid())}`, + metaData, + this.containerId, + this.context.logLevel, + ); + this.#domSelecter = ''; + this.#isFrameReady = false; + this.#readyToMount = true; + bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.iframe.name, (data) => { + this.iframe.setIframeHeight(data.height); + }); + this.#eventEmitter.on(`${CUSTOM_ERROR_MESSAGES}:${this.containerId}`, (data) => { + if (data?.errorMessages) { + this.customerErrorMessages = data.errorMessages as Record; + } + }); + } + + getID() { + return this.#elementId; + } + + mount(domElementSelector: HTMLElement | string) { + if (!domElementSelector) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); + if ( + this.metaData?.clientJSON?.config?.options?.trackMetrics + && this.metaData.clientJSON.config?.options?.trackingKey + ) { + pushElementEventWithTimeout(this.#elementId); + } + + this.#readyToMount = true; + if (this.#readyToMount) { + this.iframe.mount(domElementSelector, undefined, { + record: JSON.stringify({ + ...this.metaData, + record: this.recordData, + context: this.context, + containerId: this.containerId, + }), + }); + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + if (this.recordData.skyflowID) { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.containerId, + { + skyflowID: this.recordData.skyflowID, + containerId: this.containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } else { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.containerId, + { + id: this.recordData.token, + containerId: this.containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } + if (Object.prototype.hasOwnProperty.call(this.recordData, 'skyflowID')) { + bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.iframe.name, + {}, (payload:any) => { + this.iframe.setIframeHeight(payload.height); + }); + } + }); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.READY); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_START_TIME, Date.now()); + } + } + + iframeName(): string { + return this.iframe.name; + } + + isMounted():boolean { + return this.#isMounted; + } + + hasToken():boolean { + if (this.recordData.token) return true; + return false; + } + + isClientSetError():boolean { + return this.#isClientSetError; + } + + getRecordData() { + return this.recordData; + } + + setErrorOverride(clientErrorText: string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.iframe.name, { + name: this.iframe.name, + isTriggerError: true, + clientErrorText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.iframe.name, { + name: this.iframe.name, + isTriggerError: true, + clientErrorText, + }); + }); + } + this.#isClientSetError = true; + } + + setError(clientErrorText:string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.iframe.name, { + name: this.iframe.name, + isTriggerError: true, + clientErrorText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.iframe.name, { + name: this.iframe.name, + isTriggerError: true, + clientErrorText, + }); + }); + } + this.#isClientSetError = true; + } + + resetError() { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.iframe.name, { + name: this.iframe.name, + isTriggerError: false, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.iframe.name, { + name: this.iframe.name, + isTriggerError: false, + }); + }); + } + this.#isClientSetError = false; + } + + setAltText(altText:string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }); + }); + } + } + + clearAltText() { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + }); + } + } + + setToken(token:string) { + this.recordData = { + ...this.recordData, + token, + }; + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, + updatedValue: token, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, + updatedValue: token, + }); + }); + } + } + + unmount() { + if (this.recordData.skyflowID) { + this.#isMounted = false; + this.iframe.container?.remove(); + } + this.#isMounted = false; + this.iframe.unmount(); + } + + update(options: TInput) { + this.recordData = { + ...this.recordData, + ...options, + }; + + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.iframe.name, { + name: this.iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }); + }); + } + } +} + +export default RevealElement; diff --git a/core/external/skyflow-container.ts b/core/external/skyflow-container.ts new file mode 100644 index 000000000..b78198485 --- /dev/null +++ b/core/external/skyflow-container.ts @@ -0,0 +1,78 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Shared controller-frame bootstrap base. Stands up the SKYFLOW_FRAME_CONTROLLER +// iframe, hands it the client/context when it reports ready, and exposes +// `isControllerFrameReady`. Boundary-clean (@core imports only). It carries NO +// pure-JS methods — privacyDB's SkyflowContainer subclass adds +// insert/update/delete/get/getById/detokenize; flowDB (elements-only) re-exports +// this base as-is. +import bus from 'framebus'; +import iframer, { + getIframeSrc, + setAttributes, + setStyles, +} from '@core/iframe-libs/iframer'; +import properties from '@core/properties'; +import { + CONTROLLER_STYLES, + ELEMENT_EVENTS_TO_IFRAME, + SKYFLOW_FRAME_CONTROLLER, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import Client from '@core/client'; +import { printLog, parameterizedString } from '@core/utils/logs-helper'; +import { Context, MessageType } from '@core/types'; + +const CLASS_NAME = 'SkyflowContainer'; +class SkyflowContainer { + protected containerId: string; + + protected client: Client; + + isControllerFrameReady: boolean = false; + + protected context: Context; + + constructor(client: Client, context: Context) { + this.client = client; + this.containerId = this.client.toJSON()?.metaData?.uuid || ''; + this.context = context; + const clientDomain = window.location.origin || ''; + const iframe = iframer({ + name: `${SKYFLOW_FRAME_CONTROLLER}:${this.containerId}:${btoa(clientDomain)}:${!!this.client.toJSON()?.config?.options?.trackingKey}`, + referrer: clientDomain, + }); + setAttributes(iframe, { + src: getIframeSrc(), + }); + setStyles(iframe, { ...CONTROLLER_STYLES }); + document.body.append(iframe); + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.CAPTURE_PUREJS_FRAME, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + callback({ + client: this.client, + context, + }); + this.isControllerFrameReady = true; + }); + printLog(parameterizedString(logs.infoLogs.PUREJS_CONTROLLER_INITIALIZED, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + } + + // Only `isControllerFrameReady` belongs in the serialized metadata that rides + // the element iframe `src` URL. `client`/`containerId`/`context` are `protected` + // (so subclasses can reach them), which makes them enumerable at runtime and + // would otherwise leak the whole config into the URL. Restricting JSON.stringify + // here restores the pre-split serialized shape at every mount/serialize path at + // once, while leaving the live object client code reads untouched. + toJSON() { + return { isControllerFrameReady: this.isControllerFrameReady }; + } +} +export default SkyflowContainer; diff --git a/src/utils/helpers/index.ts b/core/helpers/index.ts similarity index 62% rename from src/utils/helpers/index.ts rename to core/helpers/index.ts index 9d5b4db53..a0308385a 100644 --- a/src/utils/helpers/index.ts +++ b/core/helpers/index.ts @@ -1,25 +1,73 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { SdkInfo } from '../../client'; +// Variant-neutral frame/element leaf helpers, shared by each package's own frame +// controller (per the loose-coupling boundary — core holds neutral helpers only, +// each package owns its tokenize()/revealData()). import { - ALLOWED_NAME_FOR_FILE, - CardType, - COPY_UTILS, DEFAULT_INPUT_FORMAT_TRANSLATION, ElementType, -} from '../../core/constants'; -import { IRevealElementOptions } from '../../core/external/reveal/reveal-container'; -import SkyflowError from '../../libs/skyflow-error'; -import { ContainerType, ISkyflow } from '../../skyflow'; -import SKYFLOW_ERROR_CODE from '../constants'; -import { detectCardType, isValidURL, validateBooleanOptions } from '../validators'; -import properties from '../../properties'; -import uuid from '../../libs/uuid'; -import SDKDetails from '../../../package.json'; + ContainerType, IValidationRule, ValidationRuleType, IRevealElementOptions, ISkyflow, +} from '@core/types'; +import SkyflowError from '@core/errors'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import { + ALLOWED_NAME_FOR_FILE, CardType, BaseElementType, COPY_UTILS, + DEFAULT_INPUT_FORMAT_TRANSLATION, CORALOGIX_DOMAIN, +} from '@core/constants'; +import properties from '@core/properties'; +import { detectCardType, validateBooleanOptions, isValidURL } from '@core/validators'; const { getType } = require('mime'); -export const flattenObject = (obj, roots = [] as any, sep = '.') => Object.keys(obj).reduce((memo, prop: any) => ({ ...memo, ...(Object.prototype.toString.call(obj[prop]) === '[object Object]' ? flattenObject(obj[prop], roots.concat([prop])) : { [roots.concat([prop]).join(sep)]: obj[prop] }) }), {}); +// Minimal structural view of a form element needed for value-match detection — +// avoids importing the concrete IFrameFormElement class from a package. +export interface IMatchableFormElement { + isMatchEqual(index: number, value: any, validation: IValidationRule): boolean; + state: { value: any }; +} +export const checkForElementMatchRule = (validations: IValidationRule[]) => { + if (!validations) return false; + for (let i = 0; i < validations.length; i += 1) { + if (validations[i].type === ValidationRuleType.ELEMENT_VALUE_MATCH_RULE) { + return true; + } + } + return false; +}; + +export const checkForValueMatch = ( + validations: IValidationRule[], + element: IMatchableFormElement, +) => { + if (!validations) return false; + for (let i = 0; i < validations.length; i += 1) { + if (validations[i].type === ValidationRuleType.ELEMENT_VALUE_MATCH_RULE) { + if (element && !element.isMatchEqual(i, element.state.value, validations[i])) { + return true; + } + } + } + return false; +}; + +// Pure expiry-date helper. Lives here (not in the package's Tier-E utils/helpers, +// which carries build-time SDK identity) so the variant-neutral validateExpiryDate +// in @core/validators — and core's internal frame layer — can reach it. +export const appendZeroToOne = (value: string) => { + if (value.length === 1 && Number(value) === 1) { + return { + isAppended: true, + value: `0${value}`, + }; + } + return { isAppended: false, value }; +}; + +// Variant-neutral frame/element leaf helpers shared by both packages' collect +// element + iframe form layers. Moved here (from each package's Tier-E +// utils/helpers) so the shared `@core/internal/iframe-form` can reach them +// without relative-importing a package sibling. Each package re-binds this set +// from its own utils/helpers. export function formatFrameNameToId(name: string) { const arr = name?.split(':'); if (arr && arr.length > 2) { @@ -33,43 +81,49 @@ export function removeSpaces(inputString:string) { return inputString.trim().replace(/[\s-]/g, ''); } +// Trim a trailing slash off the configured vault URL (Skyflow.init URL +// normalization). Variant-neutral — moved here from each package's Tier-E +// utils/helpers so both SDKs share one definition; each re-binds it locally. export function formatVaultURL(vaultURL?: string) { if (typeof vaultURL !== 'string') return vaultURL; return (vaultURL?.trim().slice(-1) === '/') ? vaultURL.slice(0, -1) : vaultURL.trim(); } -export function checkIfDuplicateExists(arr) { - return new Set(arr).size !== arr.length; -} - -export const appendZeroToOne = (value: string) => { - if (value.length === 1 && Number(value) === 1) { - return { - isAppended: true, - value: `0${value}`, - }; - } - return { isAppended: false, value }; -}; - -export const appendMonthFourDigitYears = (value: string) => { - if (value.length === 6 && Number(value.charAt(5)) === 1) { - return { isAppended: true, value: `${value.substring(0, 5)}0${value.charAt(5)}` }; +// When a valid `customElementsURL` is supplied, point the iframe secure origin at +// it (used for self-hosted element frames). Variant-neutral — moved here from +// each package's Tier-E utils/helpers, where the two definitions were identical, +// so the shared @core/external/base-skyflow init path can reach it; each package +// re-binds it locally. +export function checkAndSetForCustomUrl(config: ISkyflow) { + if ( + config?.options?.customElementsURL + && isValidURL(config?.options?.customElementsURL) + ) { + const urlString = config?.options?.customElementsURL; + const url = new URL(urlString); + const protocol = url.protocol; + const domain = url.hostname; + const fullDomain = `${protocol}//${domain}`; + properties.IFRAME_SECURE_ORIGIN = fullDomain; + properties.IFRAME_SECURE_SITE = config?.options?.customElementsURL; } - return { isAppended: false, value }; -}; +} -export const appendMonthTwoDigitYears = (value: string) => { - const lastChar = (value.length > 0 && value.charAt(value.length - 1)) || ''; - if (value.length === 4 && Number(lastChar) === 1) { - return { isAppended: true, value: `${value.substring(0, 3)}0${lastChar}` }; +// Resolve a frame name to the container kind it belongs to. Variant-neutral — +// keyed off the frame-name prefix conventions shared by both packages. +export const getContainerType = (frameName:string):ContainerType => { + const frameNameParts = frameName.split(':'); + if (frameNameParts[0] === 'reveal-composable') { + return ContainerType.COMPOSE_REVEAL; } - return { isAppended: false, value }; + return (frameNameParts[1] === 'group') + ? ContainerType.COMPOSABLE + : ContainerType.COLLECT; }; export const getReturnValue = (value: string | Blob, element: string, doesReturnValue: boolean) => { if (typeof value === 'string') { - if (element === ElementType.CARD_NUMBER) { + if (element === BaseElementType.CARD_NUMBER) { value = value && value.replace(/[\s-]/g, ''); if (!doesReturnValue) { const cardType = detectCardType(value); @@ -89,6 +143,70 @@ export const getReturnValue = (value: string | Blob, element: string, doesReturn return undefined; }; +const DANGEROUS_FILE_TYPE = ['application/zip', 'application/vnd.debian.binary-package', 'application/vnd.microsoft.portable-executable', 'application/vnd.rar']; +// Check file type and file size in KB +export const fileValidation = (value, required: Boolean = false, fileElement) => { + if (required && (value === undefined || value === '')) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_FILE_SELECTED, [], true); + } + + if (DANGEROUS_FILE_TYPE.includes(value.type)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); + } + + if (Object.prototype.hasOwnProperty.call(fileElement, 'allowedFileType') && (value !== undefined && value !== '')) { + let isValidType = false; + + if (fileElement.allowedFileType !== null && fileElement.allowedFileType !== undefined) { + fileElement.allowedFileType.forEach((type) => { + const allowedType = getType(type); + // eslint-disable-next-line max-len + if (value.type.includes(allowedType) || value.type.includes(type) || value.type.includes(type.substring(1)) || value.type.includes(type)) { + isValidType = true; + } + }); + if (!isValidType) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); + } + } + } + const sizeLimit = (Object.prototype.hasOwnProperty.call(fileElement, 'maxFileSize') && typeof fileElement.maxFileSize === 'number') + ? fileElement.maxFileSize + : 32_000_000; + if (value.size > sizeLimit) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_SIZE, [], true); + } + if (Object.prototype.hasOwnProperty.call(fileElement, 'blockEmptyFiles') && fileElement.blockEmptyFiles) { + if (value.size === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_SIZE, [], true); + } + } + + return true; +}; + +export const vaildateFileName = (name: string) => ALLOWED_NAME_FOR_FILE.test(name); + +// Variant-neutral element DOM / masking helpers shared by both packages' collect +// element + internal frame layers. Moved here (from each package's Tier-E +// utils/helpers) so the shared `@core/internal` FrameElement barrel can reach +// them without relative-importing a package sibling. Each package re-binds this +// set from its own utils/helpers. +export const appendMonthFourDigitYears = (value: string) => { + if (value.length === 6 && Number(value.charAt(5)) === 1) { + return { isAppended: true, value: `${value.substring(0, 5)}0${value.charAt(5)}` }; + } + return { isAppended: false, value }; +}; + +export const appendMonthTwoDigitYears = (value: string) => { + const lastChar = (value.length > 0 && value.charAt(value.length - 1)) || ''; + if (value.length === 4 && Number(lastChar) === 1) { + return { isAppended: true, value: `${value.substring(0, 3)}0${lastChar}` }; + } + return { isAppended: false, value }; +}; + const fns : Function[] = []; export function domReady(fn) { (() => { @@ -184,64 +302,10 @@ export const handleCopyIconClick = (textToCopy: string, domCopy: any) => { } }; -const DANGEROUS_FILE_TYPE = ['application/zip', 'application/vnd.debian.binary-package', 'application/vnd.microsoft.portable-executable', 'application/vnd.rar']; -// Check file type and file size in KB -export const fileValidation = (value, required: Boolean = false, fileElement) => { - if (required && (value === undefined || value === '')) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_FILE_SELECTED, [], true); - } - - if (DANGEROUS_FILE_TYPE.includes(value.type)) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); - } - - if (Object.prototype.hasOwnProperty.call(fileElement, 'allowedFileType') && (value !== undefined && value !== '')) { - let isValidType = false; - - if (fileElement.allowedFileType !== null && fileElement.allowedFileType !== undefined) { - fileElement.allowedFileType.forEach((type) => { - const allowedType = getType(type); - // eslint-disable-next-line max-len - if (value.type.includes(allowedType) || value.type.includes(type) || value.type.includes(type.substring(1)) || value.type.includes(type)) { - isValidType = true; - } - }); - if (!isValidType) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); - } - } - } - const sizeLimit = (Object.prototype.hasOwnProperty.call(fileElement, 'maxFileSize') && typeof fileElement.maxFileSize === 'number') - ? fileElement.maxFileSize - : 32_000_000; - if (value.size > sizeLimit) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_SIZE, [], true); - } - if (Object.prototype.hasOwnProperty.call(fileElement, 'blockEmptyFiles') && fileElement.blockEmptyFiles) { - if (value.size === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_SIZE, [], true); - } - } - - return true; -}; - -export const vaildateFileName = (name: string) => ALLOWED_NAME_FOR_FILE.test(name); - export const styleToString = (style) => Object.keys(style).reduce((acc, key) => ( `${acc + key.split(/(?=[A-Z])/).join('-').toLowerCase()}:${style[key]};` ), ''); -export const getContainerType = (frameName:string):ContainerType => { - const frameNameParts = frameName.split(':'); - if (frameNameParts[0] === 'reveal-composable') { - return ContainerType.COMPOSE_REVEAL; - } - return (frameNameParts[1] === 'group') - ? ContainerType.COMPOSABLE - : ContainerType.COLLECT; -}; - export const addSeperatorToCardNumberMask = ( cardNumberMask: any, seperator?: string, @@ -252,6 +316,38 @@ export const addSeperatorToCardNumberMask = ( return cardNumberMask; }; +// Variant-neutral frame-name / token / reveal-option helpers. Moved here (from +// each package's utils/helpers) so the shared @core reveal-frame base can reach +// them under the core ⇏ packages boundary. Each package re-binds them locally as +// `const x = coreHelpers.x` so existing importers and jest.spyOn(helpers, …) are +// unchanged. +export const getValueFromName = (name: string, index: number) => { + const names = name.split(':'); + const value = names.length > index ? names[index] : ''; + return value; +}; + +export const getAtobValue = (encodedValue: string) => { + try { + const decodedValue = atob(encodedValue); + return decodedValue; + } catch (err) { + return ''; + } +}; + +// Injects the Coralogix RUM +``` + + +Using npm + +``` +npm install skyflow-flowvault-js +``` + +--- + +# Initializing Skyflow.js +Use the `init()` method to initialize a Skyflow client as shown below. +```javascript +import Skyflow from 'skyflow-flowvault-js' // If using script tag, this line is not required. + +const skyflowClient = Skyflow.init({ + vaultID: 'string', // Id of the vault that the client should connect to. + vaultURL: 'string', // URL of the vault that the client should connect to. + getBearerToken: helperFunc, // Helper function that retrieves a Skyflow bearer token from your backend. + options: { + logLevel: Skyflow.LogLevel, // Optional, if not specified default is ERROR. + env: Skyflow.Env // Optional, if not specified default is PROD. + } +}); +``` +For the `getBearerToken` parameter, pass in a helper function that retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. A sample implementation is shown below: + +For example, if the response of the consumer tokenAPI is in the below format + +``` +{ + "accessToken": string, + "tokenType": string +} + +``` +then, your getBearerToken Implementation should be as below + +```javascript +const getBearerToken = () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4) { + if (Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } else { + reject('Error occured'); + } + } + }; + + Http.onerror = error => { + reject('Error occured'); + }; + + const url = 'https://api.acmecorp.com/skyflowToken'; + Http.open('GET', url); + Http.send(); + }); +}; + +``` +For `logLevel` parameter, there are 4 accepted values in Skyflow.LogLevel + +- `DEBUG` + + When `Skyflow.LogLevel.DEBUG` is passed, all level of logs will be printed(DEBUG, INFO, WARN, ERROR). + +- `INFO` + + When `Skyflow.LogLevel.INFO` is passed, INFO logs for every event that has occurred during the SDK flow execution will be printed along with WARN and ERROR logs. + + +- `WARN` + + When `Skyflow.LogLevel.WARN` is passed, WARN and ERROR logs will be printed. + +- `ERROR` + + When `Skyflow.LogLevel.ERROR` is passed, only ERROR logs will be printed. + +`Note`: + - The ranking of logging levels is as follows : DEBUG < INFO < WARN < ERROR + - since `logLevel` is optional, by default the logLevel will be `ERROR`. + + + +For `env` parameter, there are 2 accepted values in Skyflow.Env + +- `PROD` +- `DEV` + + In [Event Listeners](#event-listener-on-collect-elements), actual value of element can only be accessed inside the handler when the `env` is set to `DEV`. + +`Note`: + - since `env` is optional, by default the env will be `PROD`. + - Use `env` option with caution, make sure the env is set to `PROD` when using `skyflow-flowvault-js` in production. + +--- +# Quick Start + +The minimum code needed to collect a card number and get back a token: + +```javascript +import Skyflow from 'skyflow-flowvault-js'; + +const skyflowClient = Skyflow.init({ + vaultID: 'VAULT_ID', + vaultURL: 'VAULT_URL', + getBearerToken: myGetBearerTokenFunction, +}); + +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumberElement = container.create({ + tableName: 'cards', + column: 'cardNumber', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +cardNumberElement.mount('#cardNumber'); +// Assumes a
    exists on the page + +document.getElementById('submit').addEventListener('click', () => { + container.collect() + .then((response) => console.log(response.records)) + .catch((error) => console.log(error)); +}); +``` + +Everything below expands on each step: styling, validation, upsert, composable layouts, and reveal. + +--- + +# Securely collecting data client-side +- [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) +- [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) +- [**Bin lookup**](#bin-lookup) +- [**Using validations on Collect Elements**](#validations) +- [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) +- [**UI Error for Collect Elements**](#ui-error-for-collect-elements) +- [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) +- [**Update Collect Elements**](#update-collect-elements) + +## Using Skyflow Elements to collect data + +**Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. These elements are hosted by Skyflow and injected into your web page as iFrames. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements on your web page. + +### Step 1: Create a container + +First create a container for the form elements using the `container(Skyflow.ContainerType)` method of the Skyflow client as show below: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element + +A Skyflow collect Element is defined as shown below: + +```javascript +const collectElement = { + tableName: 'string', // Optional, the table this data belongs to. + column: 'string', // Optional, the column into which this data should be inserted. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: 'string', // Optional, label for the form element. + placeholder: 'string', // Optional, placeholder for the form element. + validations: [], // Optional, array of validation rules. +} +``` +The `tableName` and `column` fields indicate which table and column in the vault the Element corresponds to. + +**Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) + +The `inputStyles` field accepts a style object which consists of CSS properties that should be applied to the form element in the following states: +* `base`: all variants inherit from these styles +* `complete`: applied when the Element has valid input +* `empty`: applied when the Element has no input +* `focus`: applied when the Element has focus +* `invalid`: applied when the Element has invalid input +* `cardIcon`: applied to the card type icon in CARD_NUMBER Element +* `copyIcon`: applied to copy icon in Elements when enableCopy option is true +* `global`: used for global styles like font-family. + +Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1). + +An example of a inputStyles object: +```javascript +inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + '&:hover': { // Hover styles. + borderColor: 'green' + }, + fontFamily: '"Roboto", sans-serif' + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + copyIcon: { + position: 'absolute', + right: '8px', + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` +The states that are available for `labelStyles` are `base`, `focus`, `global` and `requiredAsterisk`. +* `requiredAsterisk`: styles applied for the Asterisk symbol in the label. + +An example of a labelStyles object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + focus: { + color: '#1d1d1d', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + requiredAsterisk:{ + color: 'red' + } +}, +``` + +The state that is available for `errorTextStyles` are `base` and `global`, it shows up when there is some error in the collect element. + +An example of a errorTextStyles object: + +```javascript +errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +Finally, the `type` field takes a Skyflow ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 8 types: +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` +- `CVV` +- `INPUT_FIELD` +- `PIN` + + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. For information on validations, see [validations](#validations). + +Along with CollectElement we can define other options which takes a object of optional parameters as described below: + +```javascript +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether a card icon should be enabled (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon to collect elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. + cardMetadata: {}, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). + masking: true, // Optional, indicates whether the input should be masked. Defaults to 'false'. + maskingChar: '*', // Optional, character used for masking input when masking is enabled. Defaults to '*'. +}; +``` + +`required`: Indicates whether the field is marked as required or not. If not provided, it defaults to false. + +`enableCardIcon` : Indicates whether the icon is visible for the CARD_NUMBER element. Defaults to true. + +`enableCopy` : Indicates whether the copy icon is visible in collect and reveal elements. + +`format`: A string value that indicates the format pattern applicable to the element type. +Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. + - For INPUT_FIELD elements, + - the length of `format` determines the expected length of the user input. + - if `translation` isn't specified, the `format` value is considered a string literal. + +`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. + +Accepted values by element type: + +| Element type | `format`and `translation` values | Examples | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | +| EXPIRATION_DATE |
  • `format`
    • `mm/yy` (default)
    • `mm/yyyy`
    • `yy/mm`
    • `yyyy/mm`
    |
    • 12/27
    • 12/2027
    • 27/12
    • 2027/12
    | +| EXPIRATION_YEAR |
  • `format`
    • `yy` (default)
    • `yyyy`
    |
    • 27
    • 2027
    | +| CARD_NUMBER |
  • `format`
    • `XXXX XXXX XXXX XXXX` (default)
    • `XXXX-XXXX-XXXX-XXXX`
    |
    • 1234 5678 9012 3456
    • 1234-5678-9012-3456
    | +| INPUT_FIELD |
  • `format`: A string that matches the desired output, with placeholder characters of your choice.
  • `translation`: An object of key/value pairs. Defaults to `{"X": "[0-9]"}`
  • | With a `format` of `+91 XXXX-XX-XXXX` and a `translation` of `[ "X": "[0-9]"]`, user input of "1234121234" displays as "+91 1234-12-1234". | + +`cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow accept card types based on which SDK will display card brand choice dropdown in the card number element. `Skyflow.CardType` is an enum with all skyflow supported card schemes. + +```javascript +import Skyflow from 'skyflow-flowvault-js' + +const cardMetadata = { + scheme: Skyflow.CardType [] // Optional, array of skyflow supported card types. +} +``` + +
    Supported card types by Skyflow.CardType :
    + +- `VISA` +- `MASTERCARD` +- `AMEX` +- `DINERS_CLUB` +- `DISCOVER` +- `JCB` +- `MAESTRO` +- `UNIONPAY` +- `HIPERCARD` +- `CARTES_BANCAIRES` + +**Collect Element Options examples for INPUT_FIELD** +Example 1 +```js +const options = { + required: true, + enableCardIcon: true, + format:'+91 XXXX-XX-XXXX', + translation: { 'X': '[0-9]' } +} +``` + +User input: "1234121234" +Value displayed in INPUT_FIELD: "+91 1234-12-1234" + +Example 2 +```js +const options = { + required: true, + enableCardIcon: true, + format: 'AY XX-XXX-XXXX', + translation: { 'X': '[0-9]', 'Y': '[A-Z]' } +} +``` + +User input: "B1234121234" +Value displayed in INPUT_FIELD: "AB 12-341-2123" + +`masking` : A boolean value for whether to mask the input of the element. When masking is enabled, user input will be replaced with a masking character. +The default masking character is `*`, but you can customize masking character using the maskingChar property. + +`maskingChar`: A single character used to mask the input when masking is enabled. Defaults to `*`, but can be customized to any character of your choice. + +Collect Element Options examples with masking: + +Example for CVV: +```js +const options = { + required: true, + enableCopy: false, + masking: true, + maskingChar: '•', +} +``` +User input: "1234" +Value displayed in CVV: "••••" + +Example for CARDHOLDER_NAME: +```js +const options = { + required: true, + enableCopy: false, + masking: true, +} +``` +User input: "John Doe" +Value displayed in CARDHOLDER_NAME: "********" + +Example for CARD_NUMBER: +```js +const options = { + required: true, + enableCopy: false, + masking: true, + maskingChar: '#' +} +``` +User input: "4111 1111 1111 1111" +Value displayed in CARD_NUMBER: "#### #### #### ####" + +Example for PIN: +```js +const options = { + required: true, + enableCopy: false, + masking: true, + maskingChar: '&' +} +``` +User input: "98364721" +Value displayed in PIN: "&&&&&&&&" + +**Note**: +- Unmasked data will be stored in the vault. + +Once the Element object and options has been defined, add it to the container using the `create(element, options)` method as shown below. The `element` param takes a Skyflow Element object and options as defined above: + +```javascript +const collectElement = { + tableName: 'string', // Optional, the table this data belongs to. + column: 'string', // Optional, the column into which this data should be inserted. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: 'string', // Optional, label for the form element. + placeholder: 'string', // Optional, placeholder for the form element. + altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. +} + +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. +}; + +const element = container.create(collectElement, options); +``` + +### Step 3: Mount Elements to the DOM + +To specify where the Elements will be rendered on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has 4 empty divs with unique ids as placeholders for 4 Skyflow Elements. + +```html +
    +
    +
    +
    +
    +
    +
    +
    + + +``` + +Now, when the `mount(domElement)` method of the Element is called, the Element will be inserted in the specified div. For instance, the call below will insert the Element into the div with the id "#cardNumber". + +```javascript +element.mount('#cardNumber'); +``` +you can use the `unmount` method to reset any collect element to it's initial state. +```javascript +element.unmount(); +``` + +### Step 4: Collect data from Elements + +When the form is ready to be submitted, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: + +- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format. +- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. + +```javascript +const options = { + additionalFields: { + records: [ + { + tableName: 'string', // Table into which record should be inserted. + data: { + column1: 'value', // Column names should match vault column names. + // ...additional fields here. + }, + skyflowId: 'string', // Optional, skyflowId of the record to update. + }, + // ...additional records here. + ], + }, // Optional + upsert: [ // Upsert operations support in the vault + { + tableName: 'string', // Table name + uniqueColumns: ['string'], // Unique columns in the table + updateType: Skyflow.UpdateType.UPDATE, // Optional, one of 'UPDATE' or 'REPLACE' + }, + ], // Optional +}; + +container.collect(options); +``` + +### End to end example of collecting data with Skyflow Elements + +**[Sample Code:](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements.html)** + +```javascript +//Step 1 +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +//Step 2 +const element = container.create({ + tableName: 'cards', + column: 'cardNumber', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'Card Number', + label: 'card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Step 3 +element.mount('#cardNumber'); // Assumes there is a div with id='#cardNumber' in the webpage. + +// Step 4 + +const nonPCIRecords = { + records: [ + { + tableName: 'cards', + data: { + gender: 'MALE', + }, + }, + ], +}; + +container.collect({ + additionalFields: nonPCIRecords, +}); + +``` + +**Sample Response :** +```javascript +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "cardNumber": [ + { "token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "nondeterministic" } + ], + "gender": [ + { "token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "nondeterministic" } + ] + }, + "httpCode": 200 + } + ] +} +``` +### Collect example with upsert support +**Sample Code** + + ```javascript +//Step 1 +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) + +//Step 2 +const cardNumberElement = container.create({ + tableName: 'cards', + column: 'card_number', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon:{ + position: 'absolute', + left:'8px', + bottom:'calc(50% - 12px)' + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold' + } + }, + errorTextStyles: { + base: { + color: '#f44336' + } + }, + placeholder: 'Card Number', + label: 'card_number', + type: Skyflow.ElementType.CARD_NUMBER +}) + + +const cvvElement = container.create({ + tableName: 'cards', + column: 'cvv', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon:{ + position: 'absolute', + left:'8px', + bottom:'calc(50% - 12px)' + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold' + } + }, + errorTextStyles: { + base: { + color: '#f44336' + } + }, + placeholder: 'CVV', + label: 'cvv', + type: Skyflow.ElementType.CVV +}) + +// Step 3 +cardNumberElement.mount('#cardNumber') //Assumes there is a div with id='#cardNumber' in the webpage. +cvvElement.mount('#cvv'); //Assumes there is a div with id='#cvv' in the webpage. + +// Step 4 + container.collect({ + upsert: [ + { + tableName: 'cards', + uniqueColumns: ['card_number'], + } + ] +}) + ``` + **Skyflow returns tokens for the record you just inserted.** +```javascript +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "card_number": [ + { "token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "nondeterministic" } + ], + "cvv": [ + { "token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "nondeterministic" } + ] + }, + "httpCode": 200 + } + ] +} +``` + +## BIN Lookup + +Skyflow supports BIN (Bank Identification Number) lookup to help identify co-badged cards and enable card network selection. + +**What is BIN Lookup?** +A Bank Identification Number (BIN) represents the first 8 digits of a card number and identifies the issuing bank, card scheme, and country. +For co-badged cards, merchants are required to offer consumers a choice of which network to process the payment through. +You can use Skyflow's BIN Lookup API to detect such cards and provide the appropriate options to users. + +### Example: Calling the BIN Lookup API +```javascript +// Function to call Skyflow's BIN Lookup API +const binLookup = (bin) => { + const myHeaders = new Headers(); + myHeaders.append("X-skyflow-authorization", ""); // TODO: replace bearer token + myHeaders.append("Content-Type", "application/json"); + + const raw = JSON.stringify({ + "BIN": bin + }); + + const requestOptions = { + method: "POST", + headers: myHeaders, + body: raw, + redirect: "follow" + }; + + // TODO: replace with your Skyflow vault URL + return fetch(`${VAULT_URL}/v1/card_lookup`, requestOptions); +}; +``` + +**Sample Response :** +```javascript +{ + "cards_data": [ + { + "BIN": "54284800", + "issuer_name": "CREDIT MUTUEL ARKEA", + "country_code": "FR", + "currency": "", + "card_type": "Credit", + "card_category": "", + "card_scheme": "CARTES BANCAIRES" + }, + { + "BIN": "54284800", + "issuer_name": "Credit Mutuel Arkea", + "country_code": "FR", + "currency": "", + "card_type": "Credit", + "card_category": "Mastercard Standard", + "card_scheme": "MASTERCARD" + } + ] +} +``` + +### Updating the Card Element with Network Schemes +```javascript +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether a card icon should be enabled (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon to collect elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. + cardMetadata: {}, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). + masking: true, // Optional, indicates whether the input should be masked. Defaults to 'false'. + maskingChar: '*', // Optional, character used for masking input when masking is enabled. Defaults to '*'. +}; +``` + +`cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow accept card types based on which SDK will display card brand choice dropdown in the card number element. `Skyflow.CardType` is an enum with all skyflow supported card schemes. + +```javascript +import Skyflow from 'skyflow-flowvault-js' + +const cardMetadata = { + scheme: Skyflow.CardType [] // Optional, array of skyflow supported card types. +} +``` + +- By default, SDK will populate its own auto-detected card scheme. + +### Samples + +- [Card brand choice](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html): +This sample illustrates how to use Bin Lookup API and display the available card schemes. + +## Using Skyflow Elements to update data + +You can update the data in a vault with Skyflow Elements. Use the following steps to securely update data. + +### Step 1: Create a container +Create a container for the form elements using the `container(Skyflow.ContainerType)` method of the Skyflow client: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element +Create a collect element. Collect Elements are defined as follows: + +```javascript +const collectElement = { + tableName: "string", // Required, the table this data belongs to. + column: "string", // Required, the column into which this data should be updated. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: "string", // Optional, label for the form element. + placeholder: "string", // Optional, placeholder for the form element. + altText: "string", // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. + skyflowId: "string", // The skyflowId of the record to be updated. +}; +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether the element needs a card icon (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. +}; +const element = container.create(collectElement, options); +``` +The `table` and `column` fields indicate which table and column the Element corresponds to. + +`skyflowId` indicates the record that you want to update. + +**Notes:** +- Use dot-delimited strings to specify columns nested inside JSON fields (for example, `address.street.line1`) + +### Step 3: Mount Elements to the DOM +To specify where the Elements are rendered on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has three empty elements with unique IDs as placeholders for three Skyflow Elements. +```html +
    +
    +
    +
    +
    +
    +
    + + +``` +Now, when you call the `mount(domElement)` method, the Elements is inserted in the specified divs. For instance, the call below inserts the Element into the div with the id "#cardNumber". +```javascript +element.mount('#cardNumber'); +``` +Use the `unmount` method to reset a Collect Element to its initial state. +```javascript +element.unmount(); +``` + + +### Step 4: Update data from Elements +When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: +- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. +- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and columns marked as unique in the table. + +```javascript +const options = { + additionalFields: { + records: [ + { + tableName: "string", // Table into which record should be updated. + data: { + column1: "value", // Column names should match vault column names. + // ...additional fields here. + }, + skyflowId: "value", // The skyflow_id of the record to be updated. + }, + // ...additional records here. + ], + },// Optional + upsert: [ // Upsert operations support in the vault + { + tableName: "string", // Table name + uniqueColumns: ["value"], // Unique columns in the table + updateType: Skyflow.UpdateType.UPDATE, // Optional, one of 'UPDATE' or 'REPLACE' + }, + ], // Optional +}; +container.collect(options); +``` +**Note:** `skyflowId` is required if you want to update the data. If `skyflowId` isn't specified, the `collect(options?)` method creates a new record in the vault. + +### End to end example of updating data with Skyflow Elements + +**Sample Code:** + +```javascript +//Step 1 +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +//Step 2 +const cardNumberElement = container.create({ + tableName: 'cards', + column: 'cardNumber', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'Card Number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + skyflowId: '431eaa6c-5c15-4513-aa15-29f50babe882', +}); +const cardHolderNameElement = container.create({ + tableName: 'cards', + column: 'first_name', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'Card Holder Name', + label: 'Card Holder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + skyflowId: '431eaa6c-5c15-4513-aa15-29f50babe882', +}); + +// Step 3 +cardNumberElement.mount('#cardNumber'); // Assumes there is a div with id='#cardNumber' in the webpage. +cardHolderNameElement.mount('#cardHolderName'); // Assumes there is a div with id='#cardHolderName' in the webpage. + +// Step 4 +const nonPCIRecords = { + records: [ + { + tableName: 'cards', + data: { + gender: 'MALE', + }, + skyflowId: '431eaa6c-5c15-4513-aa15-29f50babe882', + }, + ], +}; + +container.collect({ + additionalFields: nonPCIRecords, +}); +``` +**Sample Response :** +```javascript +{ + "records": [ + { + "tableName": "cards", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "cardNumber": [ + { "token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "nondeterministic" } + ], + "first_name": [ + { "token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic" } + ], + "gender": [ + { "token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string" } + ] + }, + "httpCode": 200 + } + ] +} +``` + +### Validations + +Skyflow-JS provides two types of validations on Collect Elements + +#### 1. Default Validations: +Every Collect Element except of type `INPUT_FIELD` has a set of default validations listed below: +- `CARD_NUMBER`: Card number validation with checkSum algorithm(Luhn algorithm). +Available card lengths for defined card types are [12, 13, 14, 15, 16, 17, 18, 19]. +A valid 16 digit card number will be in the format - `XXXX XXXX XXXX XXXX` +- `CARD_HOLDER_NAME`: Name should be 2 or more symbols, valid characters should match pattern - `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$` +- `CVV`: Card CVV can have 3-4 digits +- `EXPIRATION_DATE`: Any date starting from current month. By default valid expiration date should be in short year format - `MM/YY` +- `PIN`: Can have 4-12 digits + +#### 2. Custom Validations: +Custom validations can be added to any element which will be checked after the default validations have passed. The following Custom validation rules are currently supported: +- `REGEX_MATCH_RULE`: You can use this rule to specify any Regular Expression to be matched with the input field value + +```javascript +const regexMatchRule = { + type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE, + params: { + regex: RegExp, + error: string // Optional, default error is 'VALIDATION FAILED'. + } +} +``` + +- `LENGTH_MATCH_RULE`: You can use this rule to set the minimum and maximum permissible length of the input field value + +```javascript +const lengthMatchRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min : number, // Optional. + max : number, // Optional. + error: string // Optional, default error is 'VALIDATION FAILED'. + } +} +``` + +- `ELEMENT_VALUE_MATCH_RULE`: You can use this rule to match the value of one element with another element + +```javascript +const elementValueMatchRule = { + type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + element: CollectElement, + error: string // Optional, default error is 'VALIDATION FAILED'. + } +} +``` + +The Sample [code snippet](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-flowvault-js/samples/using-script-tag/custom-validations.html) for using custom validations: + +```javascript +/* + A simple example that illustrates custom validations. + Adding REGEX_MATCH_RULE , LENGTH_MATCH_RULE to collect element. +*/ + +// This rule allows 1 or more alphabets. +const alphabetsOnlyRegexRule = { + type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE, + params: { + regex: /^[A-Za-z]+$/, + error: 'Only alphabets are allowed', + }, +}; + +// This rule allows input length between 4 and 6 characters. +const lengthRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 4, + max: 6, + error: 'Must be between 4 and 6 alphabets', + }, +}; + +const cardHolderNameElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.INPUT_FIELD, + validations: [alphabetsOnlyRegexRule, lengthRule], +}); + +/* + Reset PIN - A simple example that illustrates custom validations. + The below code shows an example of ELEMENT_VALUE_MATCH_RULE +*/ + +// For the PIN element +const pinElement = collectContainer.create({ + label: 'PIN', + placeholder: '****', + type: Skyflow.ElementType.PIN, +}); + +// This rule allows to match the value with pinElement. +const elementMatchRule = { + type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + element: pinElement, + error: 'PIN does not match', + }, +}; + +const confirmPinElement = collectContainer.create({ + label: 'Confirm PIN', + placeholder: '****', + type: Skyflow.ElementType.PIN, + validations: [elementMatchRule], +}); + +// Mount elements on screen - errors will be shown if any of the validaitons fail. +pinElement.mount('#collectPIN'); +confirmPinElement.mount('#collectConfirmPIN'); + +``` +### Event Listener on Collect Elements + + +Helps to communicate with Skyflow elements / iframes by listening to an event + +```javascript +element.on(Skyflow.EventName,handler:function) +``` + +There are 4 events in `Skyflow.EventName` +- `CHANGE` + Change event is triggered when the Element's value changes. + +- `READY` + Ready event is triggered when the Element is fully rendered + +- `FOCUS` + Focus event is triggered when the Element gains focus + +- `BLUR` + Blur event is triggered when the Element loses focus. + +The handler ```function(state) => void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. + +```javascript +state : { + elementType: Skyflow.ElementType + isEmpty: boolean + isFocused: boolean + isValid: boolean + value: string + selectedCardScheme: Skyflow.CardType // only for CARD_NUMBER element type +} +``` + +**Note:** +- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. +- `selectedCardScheme` will exist for `CARD_NUMBER` element state and the value of Skyflow.CardType will be only populated when cardbrand choice selection is triggered otherwise, it will always be an empty string. + +##### Sample [code snippet](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-flowvault-js/samples/using-script-tag/collect-element-listeners.html) for using listeners +```javascript +// Create Skyflow client. +const skyflowClient = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => {}, + options: { + env: Skyflow.Env.DEV, + }, +}); + +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardHolderName = container.create({ + tableName: 'pii_fields', + column: 'first_name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); +const cardNumber = container.create({ + tableName: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +cardNumber.mount('#cardNumberContainer'); +cardHolderName.mount('#cardHolderNameContainer'); + +// Subscribing to CHANGE event, which gets triggered when element changes. +cardHolderName.on(Skyflow.EventName.CHANGE, state => { + // Your implementation when Change event occurs. + console.log(state); +}); + +// Subscribing to CHANGE event, which gets triggered when element changes. +cardNumber.on(Skyflow.EventName.CHANGE, state => { + // Your implementation when Change event occurs. + console.log(state); +}); + +``` +##### Sample Element state object when `env` is `DEV` + +```javascript +{ + elementType: 'CARDHOLDER_NAME', + isEmpty: false, + isFocused: true, + isValid: false, + value: 'John', +}; +{ + elementType: 'CARD_NUMBER', + isEmpty: false, + isFocused: true, + isValid: false, + value: '4111-1111-1111-1111', +}; +``` +##### Sample Element state object when `env` is `PROD` + +```javascript +{ + elementType: 'CARDHOLDER_NAME', + isEmpty: false, + isFocused: true, + isValid: false, + value: '', +}; +{ + elementType: 'CARD_NUMBER', + isEmpty: false, + isFocused: true, + isValid: false, + value: '4111-1111-XXXX-XXXX', +}; + +``` + +### UI Error for Collect Elements + +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: string)` method is used to set the error text for the element, when this method is triggered, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is triggered on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +##### Sample code snippet for setError and resetError + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + tableName: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Set custom error. +cardNumber.setError('custom error'); + +// Reset custom error. +cardNumber.resetError(); +``` + +### Override default error messages + +You can override the default error messages with custom ones by using `setErrorOverride`. This is especially useful to override default error messages in non-English languages. + +`setErrorOverride(message: string)` + +`setErrorOverride` overrides the default error message. When the value is invalid, the error resets automatically when the value becomes valid. + +##### Sample code snippet for setErrorOverride + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + tableName: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// override default error. +cardHolderNameElement.on(Skyflow.EventName.BLUR, state=>{ + if(state.isEmpty) { + //can override the message when the field is required and empty + cardHolderNameElement.setErrorOverride('custom error for required'); + } else if(!state.isValid) { + //can override the message when the input is invalid + cardHolderName.setErrorOverride('custom error for invalid'); + } +}); +``` + +##### Difference between setError and setErrorOverride: + +- `setError` sets the error state on the collect element, regardless of the element's state and value (valid or invalid). Once you call `setError`, the element remains in the error state until you call `resetError`. Use `setError` to set the error state on collect element based on server-side validations. + +- `setErrorOverride` overrides the default error message. The error message resets automatically once the value becomes valid. Use `setErrorOverride` to change the default error message for a collect element. + +**Note**: +- `setErrorOverride` can only override default error messages. +- `setErrorOverride` can only be used in BLUR event listener as shown in the earlier example. + + +### Set and Clear value for Collect Elements (DEV ENV ONLY) + +`setValue(value: string)` method is used to set the value of the element. This method will override any previous value present in the element. + +`clearValue()` method is used to reset the value of the element. + +`Note:` This methods are only available in DEV env for testing/developmental purposes and MUST NOT be used in PROD env. + +##### Sample code snippet for setValue and clearValue + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + tableName: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Set a value programatically. +cardNumber.setValue('4111111111111111'); + +// Clear the value. +cardNumber.clearValue(); + +``` + +### Update Collect Elements + +You can update collect element properties with the `update` interface. + +The `update` interface takes the below object: + +```javascript +const updateElement = { + tableName: 'string', // Optional. The table this data belongs to. + column: 'string', // Optional. The column this data belongs to. + inputStyles: {}, // Optional. Styles applied to the form element. + labelStyles: {}, // Optional. Styles for the label of the element. + errorTextStyles: {}, // Optional. Styles for the errorText of element. + label: 'string', // Optional. Label for the form element. + placeholder: 'string', // Optional. Placeholder for the form element. + validations: [], // Optional. Array of validation rules. + skyflowId: 'string' // Optional. SkyflowId of the record. +}; +``` + +Only include the properties that you want to update for the specified collect element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +`Note`: You can't update the `type` property of an element. + +### End to end example +```javascript +// Create a collect container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: {}, + }, +}; + +// Create collect elements +const cardHolderNameElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...stylesOptions, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); + +const cardNumberElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'card_number', + ...stylesOptions, + placeholder: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const cvvElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'cvv', + ...stylesOptions, + placeholder: 'CVV', + type: Skyflow.ElementType.CVV, +}); + +// Mount the collect elements. +cardHolderNameElement.mount('#cardHolderNameElement'); // Assumes there is a div with id='#cardHolderNameElement' in the webpage. +cardNumberElement.mount('#cardNumberElement'); // Assumes there is a div with id='#cardNumberElement' in the webpage. +cvvElement.mount('#cvvElement'); // Assumes there is a div with id='#cvvElement' in the webpage. + +// ... + +// Update validations property on cvvElement. +cvvElement.update({ + validations: [{ + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: 'cvv must be 3 digits', + }, + }] +}) + +// Update label, placeholder properties on cardHolderNameElement. +cardHolderNameElement.update({ + label: 'CARDHOLDER NAME', + placeholder: 'Eg: John' +}); + +// Update table, column, inputStyles properties on cardNumberElement. +cardNumberElement.update({ + tableName:'cards', + column:'card_number', + inputStyles:{ + base:{ + color:'blue' + } + } +}); +``` + +--- + +# Securely collecting data client-side using Composable Elements +- [**Using Skyflow Composable Elements to collect data**](#using-skyflow-composable-elements-to-collect-data) +- [**Event listener on Composable Element**](#set-an-event-listener-on-composable-elements) +- [**Event listener on Composable Container**](#set-an-event-listener-on-a-composable-container) +- [**Update Composable Elements**](#update-composable-elements) + +## Using Skyflow Composable Elements to collect data +Composable Elements combine multiple Skyflow Elements in a single iframe, letting you create multiple Skyflow Elements in a single row. The following steps create a composable element and securely collect data through it. + +### Step 1: Create a composable container + +Create a container for the composable element using the `container(Skyflow.ContainerType)` method of the Skyflow client: + +``` javascript + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE,containerOptions); +``` +Pass an options object that contains the following keys: + +1. `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. + + For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. + + `Note`: The sum of values in the layout array should be equal to the number of elements created + +2. `styles`: CSS styles to apply to the composable container. +3. `errorTextStyles`: CSS styles to apply if an error is encountered. + +```javascript +const options = { + layout: [2, 1], // Required + styles: { // Optional + base: { + border: '1px solid #DFE3EB', + padding: '8px', + borderRadius: '4px', + margin: '12px 2px', + }, + }, + errorTextStyles: { // Optional + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, +}; +``` + +### Step 2: Create Composable Elements +Composable Elements use the following schema: + +```javascript +const composableElement = { + tableName: 'string', // Optional. The table this data belongs to. + column: 'string', // Optional. The column this data belongs to. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional. Styles applied to the form element. + labelStyles: {}, // Optional. Styles for the label of the collect element. + errorTextStyles: {}, // Optional. Styles for the errorText of the collect element. + label: 'string', // Optional. Label for the form element. + placeholder: 'string', // Optional. Placeholder for the form element. + altText: 'string', // (DEPRECATED) Initial value for the collect element. + validations: [], // Optional. Array of validation rules. +} +``` +The `table` and `column` fields indicate which table and column in the vault the Element correspond to. + +Note: Use dot-delimited strings to specify columns nested inside JSON fields (for example, `address.street.line1`). + +All elements can be styled with [JSS](https://cssinjs.org/?v=v10.7.1) syntax. + +The `inputStyles` field accepts an object of CSS properties to apply to the form element in the following states: + +* `base`: all variants inherit from these styles +* `complete`: applied when the Element has valid input +* `empty`: applied when the Element has no input +* `focus`: applied when the Element has focus +* `invalid`: applied when the Element has invalid input +* `cardIcon`: applied to the card type icon in CARD_NUMBER Element +* `copyIcon`: applied to copy icon in Elements when enableCopy option is true +* `global`: used for global styles like font-family. + +An example of an `inputStyles` object: + +```javascript +inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + fontFamily: '"Roboto", sans-serif' + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + copyIcon: { + position: 'absolute', + right: '8px', + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` +The states that are available for `labelStyles` are `base`, `focus`, `global` and `requiredAsterisk`. +* `requiredAsterisk`: styles applied for the Asterisk symbol in the label. + +An example `labelStyles` object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + focus: { + color: '#1d1d1d' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` + +The JS SDK supports the following composable elements: + +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` +- `CVV` +- `INPUT_FIELD` +- `PIN` + +`Note`: Only when the entered value in the below composable elements is valid, the focus shifts automatically. The element types are: +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. For information on validations, see [validations](#validations). + +Along with the Composable Element definition, you can define additional options for the element: + +```javascript +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType) + format: String, // Optional, format for the element (only applicable currently for EXPIRATION_DATE ElementType), + enableCopy: false // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false') +} +``` + +- `required`: Whether or not the field is marked as required. Defaults to `false`. +- `enableCardIcon`: Whether or not the icon is visible for the CARD_NUMBER element. Defaults to `true`. +- `format`: Format pattern for the element. Only applicable to EXPIRATION_DATE and EXPIRATION_YEAR element types. +- `enableCopy`: Whether or not the copy icon is visible in collect and reveal elements. Defaults to `false`. + +The accepted `EXPIRATION_DATE` values are + +- `MM/YY` (default) +- `MM/YYYY` +- `YY/MM` +- `YYYY/MM` + + +The accepted `EXPIRATION_YEAR` values are + +- `YY` (default) +- `YYYY` + + +Once you define the Element object and options, add it to the container using the `create(element, options)` method: + +```javascript +const composableElement = { + tableName: 'string', // Optional, the table this data belongs to. + column: 'string', // Optional, the column into which this data should be inserted. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: 'string', // Optional, label for the form element. + placeholder: 'string', // Optional, placeholder for the form element. + altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. +} + +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType). + format: String, // Optional, format for the element (only applicable currently for EXPIRATION_DATE ElementType). + enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). +}; + +const element = container.create(composableElement, options); +``` + +### Step 3: Mount Container to the DOM +To specify where the Elements are rendered on your page, create a placeholder `
    ` element with unique `id` attribute. Use this empty `
    ` placeholder to mount the composable container. + +```javascript +
    +
    +
    +
    + + +``` +Use the composable container's `mount(domElement)` method to insert the container's Elements into the specified `
    `. For instance, the following call inserts Elements into the `
    ` with the `id "#composableContainer"`. + +```javacript +container.mount('#composableContainer'); +``` + +### Step 4: Collect data from elements + + +When the form is ready to be submitted, call the container's `collect(options?)` method. The options parameter takes an object of optional parameters as follows: +- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. +- `upsert`: To support upsert operations, the table containing the data and the columns marked as unique in that table. + +```javascript +const options = { + additionalFields: { + records: [ + { + tableName: 'string', // Table into which record should be inserted. + data: { + column1: 'value', // Column names should match vault column names. + // ...additional fields here. + }, + skyflowId: 'string', // Optional, skyflowId of the record to update. + }, + // ...additional records here. + ], + }, // Optional + upsert: [ // Upsert operations support in the vault + { + tableName: 'string', // Table name + uniqueColumns: ['string'], // Unique columns in the table + updateType: Skyflow.UpdateType.UPDATE, // Optional, one of 'UPDATE' or 'REPLACE' + }, + ], // Optional +}; +``` + +### End to end example of collecting data with Composable Elements + +```javascript +// Step 1 +const containerOptions = { + layout: [2, 1], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + }, + }, + errorTextStyles: { + base: { + color: 'red', + }, + }, +}; + +const composableContainer = skyflowClient.container( + Skyflow.ContainerType.COMPOSABLE, + containerOptions +); + +// Step 2 + +const collectStylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: {}, + }, +}; + +const cardHolderNameElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); + +const cardNumberElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const cvvElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'cvv', + ...collectStylesOptions, + placeholder: 'CVV', + type: Skyflow.ElementType.CVV, +}); + +// Step 3 +composableContainer.mount('#composableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. + +// Step 4 +composableContainer.collect(); +``` +### Sample Response: + +```javascript +{ + "records": [ + { + "tableName": "pii_fields", + "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882", + "tokens": { + "first_name": [ + { "token": "63b5eeee-3624-493f-825e-137a9336f882", "tokenGroupName": "deterministic" } + ], + "card_number": [ + { "token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "nondeterministic" } + ], + "cvv": [ + { "token": "7baf5bda-aa22-4587-a5c5-412f6f783a19", "tokenGroupName": "deterministic_string" } + ] + }, + "httpCode": 200 + } + ] +} +``` +For information on validations, see [validations](#validations). + +### Set an event listener on Composable Elements: + +You can communicate with Skyflow Elements by listening to element events: + +```javascript +element.on(Skyflow.EventName,handler:function) +``` + + +The SDK supports four events: + +- `CHANGE`: Triggered when the Element's value changes. +- `READY`: Triggered when the Element is fully rendered. +- `FOCUS`: Triggered when the Element gains focus. +- `BLUR`: Triggered when the Element loses focus. + +The handler `function(state) => void` is a callback function you provide that's called when the event is fired with a state object that uses the following schema: + +```javascript +state : { + elementType: Skyflow.ElementType + isEmpty: boolean + isFocused: boolean + isValid: boolean + value: string + selectedCardScheme: Skyflow.CardType // only for CARD_NUMBER element type +} +``` +`Note`: Events only include element values when in the state object when env is DEV. By default, value is an empty string. + +### Example Usage of Event Listener on Composable Elements + +```javascript +const containerOptions = { + layout: [1], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + } + }, + errorTextStyles: { + base: { + color: 'red' + } + } +} + +const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +const cvv = composableContainer.create({ + tableName: 'pii_fields', + column: 'primary_card.cvv', + type: Skyflow.ElementType.CVV, +}); + +composableContainer.mount('#cvvContainer'); + +// Subscribing to CHANGE event, which gets triggered when element changes. +cvv.on(Skyflow.EventName.CHANGE, state => { +// Your implementation when Change event occurs. +console.log(state); +}); +``` + +Sample Element state object when env is `DEV` + +```javascript +{ + elementType: 'CVV' + isEmpty: false + isFocused: true + isValid: false + value: '411' +} +``` + +Sample Element state object when env is `PROD` + +```javascript +{ + elementType: 'CVV' + isEmpty: false + isFocused: true + isValid: false + value: '' +} +``` + +### Update composable elements +You can update composable element properties with the `update` interface. + + +The `update` interface takes the below object: +```javascript +const updateElement = { + tableName: 'string', // Optional. The table this data belongs to. + column: 'string', // Optional. The column this data belongs to. + inputStyles: {}, // Optional. Styles applied to the form element. + labelStyles: {}, // Optional. Styles for the label of the element. + errorTextStyles: {}, // Optional. Styles for the errorText of element. + label: 'string', // Optional. Label for the form element. + placeholder: 'string', // Optional. Placeholder for the form element. + validations: [], // Optional. Array of validation rules. +}; +``` + +Only include the properties that you want to update for the specified composable element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +`Note`: You can't update the `type` property of an element. + +### End to end example +```javascript +const containerOptions = { layout: [2, 1] }; + +// Create a composable container. +const composableContainer = skyflowClient.container( + Skyflow.ContainerType.COMPOSABLE, + containerOptions +); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: {}, + }, +}; + +// Create composable elements. +const cardHolderNameElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...stylesOptions, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); + + +const cardNumberElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'card_number', + ...stylesOptions, + placeholder: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const cvvElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'cvv', + ...stylesOptions, + placeholder: 'CVV', + type: Skyflow.ElementType.CVV, +}); + +// Mount the composable container. +composableContainer.mount('#composableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. + +// ... + +// Update validations property on cvvElement. +cvvElement.update({ + validations: [{ + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: 'cvv must be 3 digits', + }, + }] +}) + +// Update label, placeholder properties on cardHolderNameElement. +cardHolderNameElement.update({ + label: 'CARDHOLDER NAME', + placeholder: 'Eg: John' +}); + +// Update table, column, inputStyles properties on cardNumberElement. +cardNumberElement.update({ + tableName:'cards', + column:'card_number', + inputStyles:{ + base:{ + color:'blue' + } + } +}); + + +``` +### Set an event listener on a composable container +Currently, the SDK supports one event: +- `SUBMIT`: Triggered when the `Enter` key is pressed in any container element. + +The handler `function(void) => void` is a callback function you provide that's called when the `SUBMIT' event fires. + +### Example +```javascript +const containerOptions = { layout: [1] } + +// Creating a composable container. +const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Creating the element. +const cvv = composableContainer.create({ + tableName: 'pii_fields', + column: 'primary_card.cvv', + type: Skyflow.ElementType.CVV, +}); + +// Mounting the container. +composableContainer.mount('#cvvContainer'); + +// Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. +composableContainer.on(Skyflow.EventName.SUBMIT, ()=> { + // Your implementation when the SUBMIT(enter) event occurs. + console.log('Submit Event Listener is being Triggered.'); +}); +``` + +# Securely revealing data client-side +- [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) +- [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) +- [**Set token for Reveal Elements**](#set-token-for-reveal-elements) +- [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) +- [**Update Reveal Elements**](#update-reveal-elements) +- [**Using Composable Reveal Elements to reveal data**](#using-composable-reveal-elements-to-reveal-data) +- [**Update Composable Reveal Elements**](#update-reveal-composable-elements) + + +## Using Skyflow Elements to reveal data + +Skyflow Elements can be used to securely reveal data in a browser without exposing your front end to the sensitive data. This is great for use cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. + +### Step 1: Create a container +To start, create a container using the `container(Skyflow.ContainerType)` method of the Skyflow client as shown below. + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL) +``` + +### Step 2: Create a reveal Element + +Then define a Skyflow Element to reveal data as shown below. + +```javascript +const revealElement = { + token: 'string', // Required, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. +}; +``` + +Note: To control the redaction applied to revealed data, pass `tokenGroupRedactions` in the `reveal(options?)` call (see [Step 4](#step-4-reveal-data)). + +The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data. But for reveal element, `inputStyles` accepts only `base` variant, `copyIcon` and `global` style objects. + +An example of a inputStyles object: + +```javascript +inputStyles: { + base: { + color: '#1d1d1d', + }, + copyIcon: { + position: 'absolute', + right: '8px', + top: 'calc(50% - 10px)', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a labelStyles object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a errorTextStyles object: + +```javascript +errorTextStyles: { + base: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +Along with RevealElementInput, you can define other options in the RevealElementOptions object as described below: +```js +const options = { + enableCopy: false, // Optional, enables the copy icon to reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {} // Optional, indicates the allowed data type value for format. +} +``` + +`format`: A string value that indicates how the reveal element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified to any character in the `format` value is considered as a string literal. + +`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `{ 'X': '[0-9]' }`. + +**Reveal Element Options examples:** +Example 1 +```js +const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const revealElementInput = { + token: '' +}; + +const options = { + format: '(XXX) XXX-XXXX', + translation: { 'X': '[0-9]'} +}; + +const revealElement = revealContainer.create(revealElementInput,options); +``` + +Value from vault: "1234121234" +Revealed Value displayed in element: "(123) 412-1234" + +Example 2: +```js +const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const revealElementInput = { + token: '' +}; + +const options = { + format: 'XXXX-XXXXXX-XXXXX', + translation: { 'X': '[0-9]' } +}; + +const revealElement = revealContainer.create(revealElementInput,options); +``` + +Value from vault: "374200000000004" +Revealed Value displayed in element: "3742-000000-00004" + +Once you've defined a Skyflow Element, you can use the `create(element)` method of the container to create the Element as shown below: + +```javascript +const element = container.create(revealElement) +``` + +### Step 3: Mount Elements to the DOM + +Elements used for revealing data are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-dom). + + +### Step 4: Reveal data +When the sensitive data is ready to be retrieved and revealed, call the `reveal(options?)` method on the container as shown below. The optional `options` parameter accepts `tokenGroupRedactions`, an array used to apply a redaction to the tokens belonging to a token group: + +```javascript +const options = { + tokenGroupRedactions: [ // Optional, redaction to apply per token group. + { + tokenGroupName: 'string', // Name of the token group. + redaction: 'plain_text', // Redaction (string) to apply to the token group. + }, + ], +}; + +container + .reveal(options) + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + + +### End to end example of all steps + +**[Sample Code:](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements.html)** +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +// Step 2. +const cardNumberElement = container.create({ + token: 'b63ec4e0-bbad-4e43-96e6-6bd50f483f75', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + label: 'card_number', + altText: 'XXXX XXXX XXXX XXXX', +}); + +const cvvElement = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'cvv', + altText: 'XXX', +}); + +const expiryDate= container.create({ + token: 'a4b24714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'expiryDate', + altText: 'MM/YYYY', +}); +// Step 3. +cardNumberElement.mount('#cardNumber'); // Assumes there is a placeholder div with id='cardNumber' on the page +cvvElement.mount('#cvv'); // Assumes there is a placeholder div with id='cvv' on the page +expiryDate.mount('#expiryDate'); // Assumes there is a placeholder div with id='expiryDate' on the page + +// Step 4. +container + .reveal() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + +The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. The revealed values are displayed in the mounted elements; the response returns per-token metadata, with any per-token failures inlined into the same `records` array. + +### Sample Response + +``` +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "nondeterministic", + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "tokenGroupName": "nondeterministic", + "httpCode": 200 + }, + { + "error": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047", + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] +} +``` + +When the entire reveal api request fails, the promise rejects with an error of the following shape: + +``` +{ + + "grpcCode": 5, + "httpCode": 404, + "message": "Vault not found.", + "httpStatus": "Not Found", + "details": [] +} +``` + +### UI Error for Reveal Elements +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: string)` method is used to set the error text for the element, when this method is triggered, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is triggered on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +##### Sample code snippet for setError and resetError + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', +}); + +// Set custom error. +cardNumber.setError('custom error'); + +// Reset custom error. +cardNumber.resetError(); +``` + +### Override default error messages + +You can override the default error messages with custom ones by using `setErrorOverride`. This is especially useful to override default error messages in non-English languages. + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', +}); + +const revealButton = document.getElementById('revealPCIData'); + +if (revealButton) { + revealButton.addEventListener('click', () => { + container.reveal().then((res) => { + //handle reveal response + }).catch((err) => { + cardNumber.setErrorOverride("custom error") + }); + }); +} +``` + +### Set token for Reveal Elements + +The `setToken(value: string)` method can be used to set the token of the Reveal Element. If no altText is set, the set token will be displayed on the UI as well. If altText is set, then there will be no change in the UI but the token of the element will be internally updated. + +##### Sample code snippet for setToken +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + altText: 'Card Number', +}); + +// Set token. +cardNumber.setToken('89024714-6a26-4256-b9d4-55ad69aa4047'); +``` +### Set and Clear altText for Reveal Elements +The `setAltText(value: string)` method can be used to set the altText of the Reveal Element. This will cause the altText to be displayed in the UI regardless of whether the token or value is currently being displayed. + +`clearAltText()` method can be used to clear the altText, this will cause the element to display the token or actual value of the element. If the element has no token, the element will be empty. +##### Sample code snippet for setAltText and clearAltText + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', +}); + +// Set altText. +cardNumber.setAltText('Card Number'); + +// Clear altText. +cardNumber.clearAltText(); + +``` + +## Update Reveal Elements + +You can update reveal element properties with the `update` interface. + +The `update` interface takes the below object: +```javascript +const updateElement = { + token: 'string', // Optional, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. +}; +``` + +Only include the properties that you want to update for the specified reveal element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +### End to end example +```javascript +// Create a reveal container. +const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: { + color: '#f44336' + }, + }, +}; + +// Create reveal elements +const cardHolderNameRevealElement = revealContainer.create({ + token: 'ed5fdd1f-5009-435c-a06b-3417ce76d2c8', + altText: 'first name', + ...stylesOptions, + label: 'Card Holder Name', +}); + +const cardNumberRevealElement = revealContainer.create({ + token: '8ee84061-7107-4faf-bb25-e044f3d191fe', + altText: 'xxxx', + ...stylesOptions, + label: 'Card Number', +}); + +// Mount the reveal elements. +cardHolderNameRevealElement.mount('#cardHolderNameRevealElement'); // Assumes there is a div with id='#cardHolderNameRevealElement' in the webpage. +cardNumberRevealElement.mount('#cardNumberRevealElement'); // Assumes there is a div with id='#cardNumberRevealElement' in the webpage. + +// ... + +// Update label, labelStyles properties on cardHolderNameRevealElement. +cardHolderNameRevealElement.update({ + label: 'CARDHOLDER NAME', + labelStyles: { + base: { + color: '#aa11aa' + } + } +}); + +// Update inputStyles, errorTextStyles properties on cardNumberRevealElement. +cardNumberRevealElement.update({ + inputStyles: { + base: { + color: '#fff', + backgroundColor: '#000', + borderColor: '#f00', + borderWidth: '5px' + } + }, + errorTextStyles: { + base: { + backgroundColor: '#000', + } + } +}); +``` + +--- + +# Securely revealing data client-side using Composable Elements + +## Using Composable Reveal Elements to reveal data + +Composable Reveal Elements combine multiple Skyflow Elements in a single iframe, letting you create multiple Skyflow Elements in a single row. The following steps create a composable reveal element and securely collect data through it. + +### Step 1: Create a composable reveal container + +Create a container for the composable reveal element using the `container(Skyflow.ContainerType)` method of the Skyflow client: + +``` javascript + const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); +``` +Pass an options object that contains the following keys: + +1. `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. + + For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. + + `Note`: The sum of values in the layout array should be equal to the number of elements created + +2. `styles`: CSS styles to apply to the reveal composable container. +3. `errorTextStyles`: CSS styles to apply if an error is encountered. + +```javascript +const containerOptions = { + layout: [2, 1], // Required + styles: { // Optional + base: { + border: '1px solid #DFE3EB', + padding: '8px', + borderRadius: '4px', + margin: '12px 2px', + }, + }, + errorTextStyles: { // Optional + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, +}; +``` + +### Step 2: Create Composable Reveal Elements +Composable Reveal Elements use the following schema: + +```javascript +const revealComposableElement = { + token: 'string', // Required, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. +}; +``` +Note: Redaction is no longer set per element. To control the redaction applied to revealed data, pass `tokenGroupRedactions` in the `reveal(options?)` call (see [Step 4](#step-4-reveal-data-1)). + +The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data. But for reveal element, `inputStyles` accepts only `base` variant, `copyIcon` and `global` style objects. + +An example of a inputStyles object: + +```javascript +inputStyles: { + base: { + color: '#1d1d1d', + }, + copyIcon: { + position: 'absolute', + right: '8px', + top: 'calc(50% - 10px)', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a labelStyles object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a errorTextStyles object: + +```javascript +errorTextStyles: { + base: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +Along with RevealElementInput, you can define other options in the RevealElementOptions object as described below: +```js +const options = { + enableCopy: false, // Optional, enables the copy icon to reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {} // Optional, indicates the allowed data type value for format. +} +``` + +`format`: A string value that indicates how the reveal element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified to any character in the `format` value is considered as a string literal. + +`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `{ 'X': '[0-9]' }`. + +**Reveal Element Options examples:** +Example 1 +```js +const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + +const revealElementInput = { + token: '' +}; + +const options = { + format: '(XXX) XXX-XXXX', + translation: { 'X': '[0-9]'} +}; + +const revealElement = revealComposableContainer.create(revealElementInput,options); +``` + +Value from vault: "1234121234" +Revealed Value displayed in element: "(123) 412-1234" + +Example 2: +```js +const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + +const revealElementInput = { + token: '' +}; + +const options = { + format: 'XXXX-XXXXXX-XXXXX', + translation: { 'X': '[0-9]' } +}; + +const revealElement = revealComposableContainer.create(revealElementInput,options); +``` + +Value from vault: "374200000000004" +Revealed Value displayed in element: "3742-000000-00004" + +Once you've defined a Skyflow Element, you can use the `create(element)` method of the container to create the Element as shown below: + +```javascript +const element = revealComposableContainer.create(revealElement) +``` + +### Step 3: Mount Container to the DOM +To specify where the Elements are rendered on your page, create a placeholder `
    ` element with unique `id` attribute. Use this empty `
    ` placeholder to mount the composable reveal container. + +```javascript +
    +
    +
    +
    + + +``` +Use the composable container's `mount(domElement)` method to insert the container's Elements into the specified `
    `. For instance, the following call inserts Elements into the `
    ` with the `id "#composableContainer"`. + +```javacript +revealComposableContainer.mount('#composableRevealContainer'); +``` + +### Step 4: Reveal data +When the sensitive data is ready to be retrieved and revealed, call the `reveal(options?)` method on the container as shown below. The optional `options` parameter accepts `tokenGroupRedactions`, an array used to apply a redaction to the tokens belonging to a token group: + +```javascript +const options = { + tokenGroupRedactions: [ // Optional, redaction to apply per token group. + { + tokenGroupName: 'string', // Name of the token group. + redaction: 'plain_text', // Redaction (string) to apply to the token group. + }, + ], +}; + +container + .reveal(options) + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + +### End to end example of reveal data with Composable Reveal Elements +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); +// Step 2. +const cardNumberElement = container.create({ + token: 'b63ec4e0-bbad-4e43-96e6-6bd50f483f75', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + label: 'card_number', + altText: 'XXXX XXXX XXXX XXXX', +}); + +const cvvElement = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'cvv', + altText: 'XXX', +}); + +const expiryDate= container.create({ + token: 'a4b24714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'expiryDate', + altText: 'MM/YYYY', +}); +// Step 3. +container.mount('#container') +// Step 4. +container + .reveal() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` +The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. The revealed values are displayed in the mounted elements; the response returns per-token metadata, with any per-token failures inlined into the same `records` array. + +### Sample Response + +``` +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "nondeterministic", + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "tokenGroupName": "nondeterministic", + "httpCode": 200 + }, + { + "error": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047", + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] +} +``` + +When the entire reveal request fails, the promise rejects with an error of the following shape: + +``` +{ + "grpcCode": 5, + "httpCode": 404, + "message": "Vault not found.", + "httpStatus": "Not Found", + "details": [] +} +``` + +## Update Reveal Composable Elements + +You can update reveal composable element properties with the `update` interface. + +The `update` interface takes the below object: +```javascript +const updateElement = { + token: 'string', // Optional, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. +}; +``` + +Only include the properties that you want to update for the specified reveal element. + +Properties your provided when you created the element remain the same until you explicitly update them. + + +### End to end example +```javascript +// Create a reveal composable container. +const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: { + color: '#f44336' + }, + }, +}; + +// Create reveal elements +const cardHolderNameRevealElement = revealComposableContainer.create({ + token: 'ed5fdd1f-5009-435c-a06b-3417ce76d2c8', + altText: 'first name', + ...stylesOptions, + label: 'Card Holder Name', +}); + +const cardNumberRevealElement = revealComposableContainer.create({ + token: '8ee84061-7107-4faf-bb25-e044f3d191fe', + altText: 'xxxx', + ...stylesOptions, + label: 'Card Number', +}); + +// Mount the reveal elements. +revealComposableContainer.mount('#container'); // Assumes there is a div with container +// ... + +// Update label, labelStyles properties on cardHolderNameRevealElement. +cardHolderNameRevealElement.update({ + label: 'CARDHOLDER NAME', + labelStyles: { + base: { + color: '#aa11aa' + } + } +}); + +// Update inputStyles, errorTextStyles properties on cardNumberRevealElement. +cardNumberRevealElement.update({ + inputStyles: { + base: { + color: '#fff', + backgroundColor: '#000', + borderColor: '#f00', + borderWidth: '5px' + } + }, + errorTextStyles: { + base: { + backgroundColor: '#000', + } + } +}); +``` + +--- + +# Reporting a Vulnerability + +If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. + + diff --git a/packages/skyflow-flowvault-js/jest.config.json b/packages/skyflow-flowvault-js/jest.config.json new file mode 100644 index 000000000..9a0dacd2a --- /dev/null +++ b/packages/skyflow-flowvault-js/jest.config.json @@ -0,0 +1,32 @@ +{ + "verbose": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "/src/**/*.{ts,tsx}", + "/../../core/**/*.{ts,tsx}", + "!**/*.d.ts", + "!/src/index.ts", + "!/src/index-node.ts", + "!/src/index-internal.ts", + "!/src/internal/internal-types/index.ts", + "!/src/utils/common/index.ts", + "!/src/utils/logs-helper/index.ts", + "!/src/external/skyflow-container.ts", + "!/src/external/collect/compose-collect-element.ts", + "!/src/external/reveal/reveal-element.ts", + "!/src/external/reveal/composable-reveal-element.ts", + "!/src/external/reveal/composable-reveal-internal.ts", + "!/src/internal/reveal/reveal-frame.ts" + ], + "testEnvironment": "jsdom", + "testTimeout": 30000, + "setupFiles": ["/tests/jest.setup.js"], + "moduleNameMapper":{ + "^@core/(.*)$": "/../../core/$1", + "^.+\\.svg$": "/tests/__mocks__/file-mock.js" + }, + "transform": { + "^.+\\.[jt]sx?$": ["babel-jest", { "rootMode": "upward" }] + }, + "transformIgnorePatterns": ["/node_modules/(?!mime)"] +} diff --git a/packages/skyflow-flowvault-js/package.json b/packages/skyflow-flowvault-js/package.json new file mode 100644 index 000000000..bf35339c7 --- /dev/null +++ b/packages/skyflow-flowvault-js/package.json @@ -0,0 +1,53 @@ +{ + "name": "skyflow-flowvault-js", + "preferGlobal": true, + "analyze": false, + "version": "1.0.0-beta.1", + "author": "Skyflow", + "description": "Skyflow FlowVault JavaScript SDK", + "homepage": "https://github.com/skyflowapi/skyflow-js", + "main": "./dist/sdkNodeBuild/index.js", + "types": "./types/packages/skyflow-flowvault-js/src/index-node.d.ts", + "files": [ + "dist/sdkNodeBuild", + "types" + ], + "license": "MIT", + "keywords": [ + "client", + "sdk", + "javascript" + ], + "scripts": { + "type-check": "tsc --noEmit", + "type-check:watch": "npm run type-check -- --watch", + "build:types": "tsc --emitDeclarationOnly && tsc-alias -p tsconfig.json", + "dev": "webpack serve --config=webpack.dev.js --open --hot ", + "build-browser-sdk": "webpack --config=webpack.skyflow-browser.js", + "build-node-sdk": "webpack --config=webpack.skyflow-node.js", + "build-iframe": "webpack --config=webpack.iframe.js", + "test": "jest --config=jest.config.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/skyflowapi/skyflow-js.git" + }, + "dependencies": { + "core-js": "3.44.0", + "framebus": "4.0.5", + "inject-stylesheet": "2.0.0", + "jquery": "3.7.1", + "jquery-mask-plugin": "1.14.16", + "jss": "10.10.0", + "jss-preset-default": "10.10.0", + "jwt-decode": "3.1.2", + "lodash": "4.18.1", + "mime": "3.0.0", + "regex-parser": "2.3.1", + "set-value": "4.1.0" + }, + "engines": { + "node": ">=12.0", + "npm": ">=6.0" + } +} diff --git a/packages/skyflow-flowvault-js/samples/README.md b/packages/skyflow-flowvault-js/samples/README.md new file mode 100644 index 000000000..2973c6386 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/README.md @@ -0,0 +1,149 @@ +# skyflow-flowvault-js samples + +Runnable samples for [`skyflow-flowvault-js`](../README.md), Skyflow's **Flow vault** JavaScript SDK. + +Test the SDK by adding your `VAULT_ID`, `VAULT_URL`, and `SERVICE-ACCOUNT` details as the corresponding values in each sample. + +> **Note:** `skyflow-flowvault-js` v1.x is **Elements-only**. There are no pure-JS (`insert`/`get`/`delete`), file-upload, file-render, or 3DS samples here — those live in the [`skyflow-js` samples](../../skyflow-js/samples/README.md). + +## Prerequisites +- A Skyflow account. If you don't have one, register for one on the [Try Skyflow](https://skyflow.com/try-skyflow) page. +- A **Flow vault**. `skyflow-flowvault-js` does not work against a PDB vault — use [`skyflow-js`](../../skyflow-js/README.md) for those. +- [Node.js](https://nodejs.org/en/) version 10 or above +- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) version 6.x.x +- [express.js](http://expressjs.com/en/starter/hello-world.html) + +## Get Started + +### Create the vault +1. Sign in to Skyflow Studio. In a browser, navigate to Skyflow Studio. +2. Create a Flow vault. +3. Once the vault is created, click the gear icon and select **Edit Vault Details**. + +To run the following commands, you'll need to retrieve your vault-specific values, **** and ****. Find your vault values by clicking the vault menu icon > Edit vault details. Note your **Vault URL** and **Vault ID** values, then click Cancel. You'll need these later. + +### Create a service account +1. In Studio, click **Settings** in the upper navigation. +2. In the side navigation, click **Vault**, then choose your vault from the dropdown menu. +3. Under in the side navigation click, **IAM**, click **> Service Accounts > New Service Account**. +4. For **Name**, enter "SDK Sample". For **Roles**, choose **Vault Editor.** +5. Click **Create**. + +### Create a service account bearer token generation endpoint +1. Create a new directory named `bearer-token-generator`. + + mkdir bearer-token-generator +2. Navigate to `bearer-token-generator` directory. + + cd bearer-token-generator +3. Initialize npm + + npm init +4. Install `skyflow-node` + + npm i skyflow-node +5. Create an `index.js` file and open the file. +6. Populate `index.js` file with below code snippet. +```javascript +const express = require('express') +const app = express() +var cors = require('cors') +const port = 3000 +const { + generateBearerToken, + isExpired +} = require('skyflow-node'); + +app.use(cors()) + +let filepath = 'cred.json'; +let bearerToken = ""; + +function getSkyflowBearerToken() { + return new Promise(async (resolve, reject) => { + try { + if (!isExpired(bearerToken)) resolve(bearerToken) + else { + let response = await generateBearerToken(filepath); + bearerToken = response.accessToken; + resolve(bearerToken); + } + } catch (e) { + reject(e); + } + }); +} + +app.get('/', async (req, res) => { + let bearerToken = await getSkyflowBearerToken(); + res.json({"accessToken" : bearerToken}); +}) + +app.listen(port, () => { + console.log(`Server is listening on port ${port}`) +}) +``` +7. Run the following command to start your local server. + + node index.js + server will start at `localhost:3000` +8. Your **** with `http://localhost:3000/` + +--- + +## Sample catalog + +Every sample exists in up to three flavors. Pick the one that matches how you consume the SDK: + +| Flavor | Directory | How the SDK is loaded | +|---|---|---| +| Script tag | [`using-script-tag/`](using-script-tag) | ` + + + + +

    Composable Elements

    +
    +
    +
    + + +
    + +
    +
    
    +        
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-npm/composable-elements-update/src/index.js b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements-update/src/index.js new file mode 100644 index 000000000..863413b4c --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements-update/src/index.js @@ -0,0 +1,329 @@ +import Skyflow from 'skyflow-flowvault-js'; + +try { + const revealView = document.getElementById('revealView'); + revealView.style.visibility = 'hidden'; + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + }); + + //custom styles for collect elements + const cardholderStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px' + }, + }, + labelStyles: { + }, + errorTextStyles: { + }, + }; + + const cardNumberStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + paddingLeft: '18px' + }, + }, + labelStyles: { + }, + errorTextStyles: { + }, + }; + + const expiryDateStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '49px' + }, + }, + labelStyles: { + }, + errorTextStyles: { + }, + }; + + const cvvStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '30px' + }, + }, + labelStyles: { + }, + errorTextStyles: { + base: { + color: 'red' + } + }, + }; + + const containerOptions = { + layout: [1, 3], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + boxShadow: '8px' + } + }, + errorTextStyles: { + base: { + color: 'red' + } + } + } + // create collect Container + const composableContainer = skyflow.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + + const cardHolderNameElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...cardholderStyles, + label: 'Cardholder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + }); + + const cardNumberElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'card_number', + ...cardNumberStyles, + type: Skyflow.ElementType.CARD_NUMBER, + placeholder: 'XXXX XXXX XXXX XXXX' + }); + + const expiryDateElement = composableContainer.create({ + tableName: 'cards', + column: 'expiry_date', + ...expiryDateStyles, + placeholder: 'MM/YY', + type: Skyflow.ElementType.EXPIRATION_DATE, + }); + + + const cvvElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'cvv', + ...cvvStyles, + placeholder: 'CVC', + type: Skyflow.ElementType.CVV, + }); + + // mount the container + composableContainer.mount('#composableContainer'); + + // Add OnSubmit event listner on composable container + composableContainer.on(Skyflow.EventName.SUBMIT, () => { + // Handle when enter key pressed in any container elements + console.log('Submit Listener is being Triggered.'); + }); + + + // Sample helper function to determine cvv length. + const findCvvLength = (cardBinValue) => { + console.log('Came here..!'); + const amexRegex = /^3[47][0-9]{4}$/ + return amexRegex.test(cardBinValue.slice(0, 6)) ? 4 : 3 + }; + + // Validation rules for cvv element. + const length3Rule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: 'cvv must be 3 digits', + }, + }; + + const length4Rule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 4, + error: 'cvv must be 4 digits', + }, + }; + + // OnChange listener for cardNumber element. + cardNumberElement.on(Skyflow.EventName.CHANGE, (state) => { + console.log('update validation', state) + if (state.isValid) { + // update cvv element validation rule. + if (findCvvLength(state.value) === 3) { + cvvElement.update({ validations: [length3Rule] }); + } + else + cvvElement.update({ validations: [length4Rule] }); + } + }); + + // update composable elements + const updateElementsButton = document.getElementById('updateElements'); + if (updateElementsButton) { + updateElementsButton.addEventListener('click', () => { + + // update label,placeholder on cardholderName, + cardHolderNameElement.update({ + label: 'CARDHOLDER NAME', + placeholder: 'Eg: John' + }); + + // update styles on card number + cardNumberElement.update({ + inputStyles: { + base: { + color: 'blue' + } + } + }); + + // update table,coloumn on expiry date + expiryDateElement.update({ + tableName: 'pii_fields', + column: 'expiry_date', + }); + + }); + } + + + + // collect all elements data + const collectButton = document.getElementById('collectPCIData'); + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse = composableContainer.collect(); + collectResponse + .then(response => { + document.getElementById('collectResponse').innerHTML = + JSON.stringify(response, null, 2); + + revealView.style.visibility = 'visible'; + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records[0].tokens; + const revealContainer = skyflow.container( + Skyflow.ContainerType.REVEAL + ); + const revealCardNumberElement = revealContainer.create({ + token: fieldsTokenData.card_number[0].token, + label: 'Card Number', + ...revealStyleOptions, + + }); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealCardCvvElement = revealContainer.create({ + token: fieldsTokenData.cvv[0].token, + label: 'Cvv', + ...revealStyleOptions, + + }); + revealCardCvvElement.mount('#revealCvv'); + + const revealCardExpiryElement = revealContainer.create({ + token: fieldsTokenData.expiry_date[0].token, + label: 'Card Expiry Date', + ...revealStyleOptions, + }); + revealCardExpiryElement.mount('#revealExpiryDate'); + + const revealCardholderNameElement = revealContainer.create({ + token: fieldsTokenData.first_name[0].token, + label: 'Card Holder Name', + ...revealStyleOptions, + }); + revealCardholderNameElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData'); + + if (revealButton) { + revealButton.addEventListener('click', () => { + revealContainer + .reveal() + .then(res => { + console.log(res); + }) + .catch(err => { + console.log(err); + }); + }); + } + }) + .catch(err => { + console.log(err); + }); + }); + } + + +} catch (err) { + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/package.json b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/package.json new file mode 100644 index 000000000..594c54f4d --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/package.json @@ -0,0 +1,16 @@ +{ + "name": "composableelements", + "version": "1.0.0", + "description": "A Sample on how to add Composable Elements ", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/src/index.html b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/src/index.html new file mode 100644 index 000000000..d4020c6ce --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/src/index.html @@ -0,0 +1,53 @@ + + + + + + + + Skyflow Elements + + + + + +

    Composable Elements

    +
    +
    +
    + +
    + +
    +
    
    +		
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/src/index.js b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/src/index.js new file mode 100644 index 000000000..9d725d399 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/src/index.js @@ -0,0 +1,257 @@ +/* + Copyright (c) 2022 Skyflow, Inc. +*/ + +import Skyflow from 'skyflow-flowvault-js'; + +try { + const revealView = document.getElementById('revealView'); + revealView.style.visibility = 'hidden'; + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + }); + + //custom styles for collect elements + const cardholderStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + labelStyles: { + }, + }; + + const cardNumberStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + paddingLeft: '18px' + }, + }, + labelStyles: { + }, + }; + + const expiryDateStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '49px' + }, + }, + labelStyles: { + }, + }; + + const cvvStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '30px' + }, + }, + labelStyles: { + }, + }; + + const containerOptions = { + layout: [1, 3], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + boxShadow: '8px' + } + }, + errorTextStyles: { + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } + } + // create collect Container + const composableContainer = skyflow.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + + const cardHolderNameElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...cardholderStyles, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + }); + + const cardNumberElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'card_number', + ...cardNumberStyles, + type: Skyflow.ElementType.CARD_NUMBER, + placeholder: 'XXXX XXXX XXXX XXXX' + }); + + const expiryDateElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'expiry_date', + ...expiryDateStyles, + placeholder: 'MM/YY', + type: Skyflow.ElementType.EXPIRATION_DATE, + }); + + + const cvvElement = composableContainer.create({ + tableName: 'pii_fields', + column: 'cvv', + ...cvvStyles, + placeholder: 'CVC', + type: Skyflow.ElementType.CVV, + }); + + // mount the container + composableContainer.mount('#composableContainer'); + + // collect all elements data + const collectButton = document.getElementById('collectPCIData'); + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse = composableContainer.collect(); + collectResponse + .then(response => { + document.getElementById('collectResponse').innerHTML = + JSON.stringify(response, null, 2); + + revealView.style.visibility = 'visible'; + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + }, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records[0].tokens; + const revealContainer = skyflow.container( + Skyflow.ContainerType.REVEAL + ); + const revealCardNumberElement = revealContainer.create({ + token: fieldsTokenData.card_number[0].token, + label: 'Card Number', + ...revealStyleOptions, + + }); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealCardCvvElement = revealContainer.create({ + token: fieldsTokenData.cvv[0].token, + label: 'Cvv', + ...revealStyleOptions, + + }); + revealCardCvvElement.mount('#revealCvv'); + + const revealCardExpiryElement = revealContainer.create({ + token: fieldsTokenData.expiry_date[0].token, + label: 'Card Expiry Date', + ...revealStyleOptions, + }); + revealCardExpiryElement.mount('#revealExpiryDate'); + + const revealCardholderNameElement = revealContainer.create({ + token: fieldsTokenData.first_name[0].token, + label: 'Card Holder Name', + ...revealStyleOptions, + }); + revealCardholderNameElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData'); + + if (revealButton) { + revealButton.addEventListener('click', () => { + revealContainer + .reveal() + .then(res => { + console.log(res); + }) + .catch(err => { + console.log(err); + }); + }); + } + }) + .catch(err => { + console.log(err); + }); + }); + } +} catch (err) { + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/package.json b/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/package.json new file mode 100644 index 000000000..1779b6213 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/package.json @@ -0,0 +1,17 @@ +{ + "name": "customvalidations", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open --no-cache", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-npm/custom-validations/src/index.html b/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/src/index.html similarity index 100% rename from samples/using-npm/custom-validations/src/index.html rename to packages/skyflow-flowvault-js/samples/using-npm/custom-validations/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/src/index.js b/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/src/index.js new file mode 100644 index 000000000..be7144490 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/src/index.js @@ -0,0 +1,141 @@ +/* + Copyright (c) 2022 Skyflow, Inc. +*/ +import Skyflow from 'skyflow-flowvault-js'; + +try{ + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options:{ + logLevel:Skyflow.LogLevel.ERROR, + env:Skyflow.Env.PROD, + } + }); + + // Create collect Container. + const collectContainer = skyflow.container(Skyflow.ContainerType.COLLECT); + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + }; + + + // Create a validation rule. + const regexRule = { + // REGEX Rule will validate the element value with the given regex + type:Skyflow.ValidationRuleType.REGEX_MATCH_RULE , + params:{ + // regex rule expects a regex to be tested on element value + regex:/[A-Za-z0-9]+/, + // specify what error text should be displayed + // when this validation rule failed + error:'only alphabets are allowed' + } + } + // Creating a length rule. + const lengthRule = { + // LENGTH match rule will validate whether the element value length matches with given length. + type:Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params:{ + // specify minimum length that element value should have + min:3, + // specify maximum length that element value should have + max:12, + // specify what error text should be displayed + // when this validation rule failed + error:'must be between 3 to 12 alphabets' + } + } + + const userNameElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + placeholder: 'Enter User Name', + label: 'User Name', + type: Skyflow.ElementType.INPUT_FIELD, + // pass validation rules + validations:[regexRule,lengthRule] + }); + + const passwordElement = collectContainer.create({ + ...collectStylesOptions, + label: 'Enter Password', + placeholder: 'Password', + type: Skyflow.ElementType.INPUT_FIELD, + }); + + const elementMatchRule = { + // ELEMENT VALUE MATCH RULE validates that element value matches the provied element. + type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + // Specify with which element value should be matched. + element: passwordElement, + // Specify what error text should be displayed + // when this validation rule failed + error: 'password doesn’t match' + } + } + + const confirmPasswordElement = collectContainer.create({ + ...collectStylesOptions, + label: 'Confirm Password', + placeholder: 'confirm password', + type: Skyflow.ElementType.INPUT_FIELD, + // Add validations. + validations:[elementMatchRule] + }); + + + // Mount the elements. + userNameElement.mount('#collectUserName'); + passwordElement.mount('#collectPassword'); + confirmPasswordElement.mount('#collectConfirmPassword'); + +}catch(err){ + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/package.json b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/package.json new file mode 100644 index 000000000..89ad4064a --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflowelements", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js new file mode 100644 index 000000000..bf0a3e3c3 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js @@ -0,0 +1,139 @@ +/* + Copyright (c) 2023 Skyflow, Inc. +*/ +import Skyflow from 'skyflow-flowvault-js'; + +try { + + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + }); + + // Create collect Container. + const collectContainer = skyflow.container(Skyflow.ContainerType.COLLECT); + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + }; + + // Create collect elements. + const cardNumberElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'primary_card.card_number', + ...collectStylesOptions, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + }, { + format: 'XXXX-XXXX-XXXX-XXXX' // inbuilt format + }); + + const ssnElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'ssn', + ...collectStylesOptions, + label: 'SSN', + placeholder: 'ssn', + type: Skyflow.ElementType.INPUT_FIELD, + }, { + format: 'XXX-XX-XXXX', + translation: { X: '[0-9]' } // translates each 'X' in format string accepts a digit ranging from 0-9. + }); + + const expiryDateElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'primary_card.expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + }, { + format: 'MM/YYYY' // inbuilt format. + }); + + const passportNumberElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'passport_number', + ...collectStylesOptions, + label: 'Passport Number', + placeholder: 'passport number', + type: Skyflow.ElementType.INPUT_FIELD, + }, { + format: 'XXYYYYYYY', + translation: { X: '[A-Z]', Y: '[0-9]' } + // translates each 'X' in format string accepts a uppercase alphabet A to Z. + // and each 'Y' in format string accepts a digit ranging from 0-9. + }); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + ssnElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + passportNumberElement.mount('#collectCardholderName'); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData'); + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse = collectContainer.collect(); + collectResponse + .then((response) => { + document.getElementById('collectResponse').innerHTML = + JSON.stringify(response, null, 2); + }) + .catch((err) => { + console.log(err); + }); + }); + } +} catch (err) { + console.log(err); +} \ No newline at end of file diff --git a/samples/using-npm/skyflow-elements-input-formatting/src/index.html b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/index.html similarity index 100% rename from samples/using-npm/skyflow-elements-input-formatting/src/index.html rename to packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js new file mode 100644 index 000000000..08b7543f4 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js @@ -0,0 +1,109 @@ +/* + Copyright (c) 2023 Skyflow, Inc. +*/ +import Skyflow from 'skyflow-flowvault-js'; + +try { + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + }); + + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + }; + + const revealContainer = skyflow.container(Skyflow.ContainerType.REVEAL); + const revealCardNumberElement = revealContainer.create({ + token: '', + label: 'Card Number', + ...revealStyleOptions, + }, { + format: 'XXXX-XXXX-XXXX-XXXX', + translation: { X: '[0-9]' } + }); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealSSNElement = revealContainer.create({ + token: '', + label: 'SSN', + ...revealStyleOptions, + altText: '###', + }, { + format: 'XX-XXX-XXXX', + }); + revealSSNElement.mount('#revealCvv'); + + const revealPhoneNumberElement = revealContainer.create({ + token: '', + label: 'Phone Number', + ...revealStyleOptions, + }, { + format: '(XXX) XXX-XXXX', + translation: { X: '[0-9]' } + }); + revealPhoneNumberElement.mount('#revealExpiryDate'); + + const revealDrivingLicenseElement = revealContainer.create({ + token: '', + label: 'Driving License', + ...revealStyleOptions, + }, { + format: 'YXX XXXX XXXX', + translation: { Y: '[A-Z]', X: '[0-9]' } + }); + revealDrivingLicenseElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData'); + + if (revealButton) { + revealButton.addEventListener('click', () => { + revealContainer.reveal().then((res) => { + console.log(res); + }).catch((err) => { + console.log(err); + }); + }); + } +} catch (err) { + console.log(err); +} diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/package.json b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/package.json new file mode 100644 index 000000000..db2c41779 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflow-elements-update-records", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-npm/skyflow-elements-update-records/src/index.html b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/src/index.html similarity index 100% rename from samples/using-npm/skyflow-elements-update-records/src/index.html rename to packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/src/index.js b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/src/index.js new file mode 100644 index 000000000..1f3c9bac8 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/src/index.js @@ -0,0 +1,155 @@ +/* + Copyright (c) 2022 Skyflow, Inc. +*/ +import Skyflow from 'skyflow-flowvault-js'; +try { + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + }); + // Create collect Container. + const collectContainer = skyflow.container(Skyflow.ContainerType.COLLECT); + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + }; + + // Create collect elements. + const cardNumberElement = collectContainer.create({ + tableName: 'table1', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'card number', + label: 'Card Number', + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + type: Skyflow.ElementType.CARD_NUMBER, + }); + + const cvvElement = collectContainer.create({ + tableName: 'table1', + column: 'cvv', + ...collectStylesOptions, + label: 'Cvv', + placeholder: 'cvv', + type: Skyflow.ElementType.CVV, + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + }); + + const expiryDateElement = collectContainer.create({ + tableName: 'table1', + column: 'expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + }); + + const cardHolderNameElement = collectContainer.create({ + tableName: 'table2', + column: 'name', + ...collectStylesOptions, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + }); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + cvvElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + cardHolderNameElement.mount('#collectCardholderName'); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData'); + const collectOptions = { + additionalFields: { + records: [ + { + tableName: 'table1', + data: { + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + gender: 'MALE', + }, + }, + { + tableName: 'table2', + data: { + gender: 'MALE', + }, + }, + ], + }, + }; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse = collectContainer.collect(collectOptions); + collectResponse + .then((response) => { + console.log(response); + document.getElementById('collectResponse').innerHTML = JSON.stringify( + response, + null, + 2 + ); + }) + .catch((err) => { + document.getElementById('collectResponse').innerHTML = JSON.stringify( + err, + null, + 2 + ); + console.log(err); + }); + }); + } +} catch (err) { + console.log(err); +} diff --git a/samples/using-npm/pure-js-delete/.gitignore b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/.gitignore similarity index 100% rename from samples/using-npm/pure-js-delete/.gitignore rename to packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/.gitignore diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/package.json b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/package.json new file mode 100644 index 000000000..06901c140 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/package.json @@ -0,0 +1,15 @@ +{ + "name": "skyflow-elements-update", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + } +} diff --git a/samples/using-npm/skyflow-elements-update/src/index.html b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/src/index.html similarity index 100% rename from samples/using-npm/skyflow-elements-update/src/index.html rename to packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/src/index.js b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/src/index.js new file mode 100644 index 000000000..76df443a4 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update/src/index.js @@ -0,0 +1,365 @@ +/* + Copyright (c) 2023 Skyflow, Inc. +*/ +import Skyflow from "skyflow-flowvault-js"; + +try { + const revealView = document.getElementById("revealView"); + revealView.style.visibility = "hidden"; + const skyflow = Skyflow.init({ + vaultID: "", + vaultURL: "", + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ""; + Http.open("GET", url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + }); + + // Create collect Container. + const collectContainer = skyflow.container(Skyflow.ContainerType.COLLECT); + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px", + fontFamily: '"Roboto", sans-serif', + }, + complete: { + color: "#4caf50", + }, + empty: {}, + focus: {}, + invalid: { + color: "#f44336", + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + requiredAsterisk: { + color: "red", + }, + }, + errorTextStyles: { + base: { + color: "#f44336", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + }; + + // Create collect elements. + const cardNumberElement = collectContainer.create( + { + tableName: "pii_fields", + column: "card_number", + ...collectStylesOptions, + placeholder: "card number", + label: "Card Number", + type: Skyflow.ElementType.CARD_NUMBER, + }, + { + required: true, + } + ); + + const cvvElement = collectContainer.create({ + tableName: "pii_fields", + column: "cvv", + ...collectStylesOptions, + label: "Cvv", + placeholder: "cvv", + type: Skyflow.ElementType.CVV, + }); + + const expiryDateElement = collectContainer.create({ + tableName: "pii_fields", + column: "expiry_date", + ...collectStylesOptions, + label: "Expiry Date", + placeholder: "MM/YYYY", + type: Skyflow.ElementType.EXPIRATION_DATE, + }); + + const cardHolderNameElement = collectContainer.create({ + tableName: "pii_fields", + column: "name", + ...collectStylesOptions, + label: "Card Holder Name", + placeholder: "cardholder name", + type: Skyflow.ElementType.CARDHOLDER_NAME, + }); + + // Mount the elements. + cardNumberElement.mount("#collectCardNumber"); + cvvElement.mount("#collectCvv"); + expiryDateElement.mount("#collectExpiryDate"); + cardHolderNameElement.mount("#collectCardholderName"); + + // Sample helper function to determine cvv length. + const findCvvLength = (cardBinValue) => { + console.log("Came here..!"); + const amexRegex = /^3[78][0-9]{4}$/; + return amexRegex.test(cardBinValue.slice(0, 6)) ? 4 : 3; + }; + + // Validation rules for cvv element. + const length3Rule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: "cvv must be 3 digits", + }, + }; + + const length4Rule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 4, + error: "cvv must be 4 digits", + }, + }; + + // OnChange listener for cardNumber element. + cardNumberElement.on(Skyflow.EventName.CHANGE, (state) => { + console.log("update validation", state); + if (state.isValid) { + // update cvv element validation rule. + if (findCvvLength(state.value) === 3) { + cvvElement.update({ validations: [length3Rule] }); + } else cvvElement.update({ validations: [length4Rule] }); + } + }); + + // update collect elements' properties + const updateCollectElementsButton = document.getElementById( + "updateCollectElements" + ); + if (updateCollectElementsButton) { + updateCollectElementsButton.addEventListener("click", () => { + // update label,placeholder on cardholderName, + cardHolderNameElement.update({ + label: "CARDHOLDER NAME", + placeholder: "Eg: John", + type: Skyflow.ElementType.PIN, + }); + + // update styles on card number + cardNumberElement.update({ + inputStyles: { + base: { + color: "blue", + }, + }, + }); + + // update table,coloumn on expiry date + expiryDateElement.update({ + tableName: "pii_fields", + column: "expiration_date", + }); + }); + } + + // Collect all elements data. + const collectButton = document.getElementById("collectPCIData"); + if (collectButton) { + collectButton.addEventListener("click", () => { + const collectResponse = collectContainer.collect(); + collectResponse + .then((response) => { + document.getElementById("collectResponse").innerHTML = JSON.stringify( + response, + null, + 2 + ); + }) + .catch((err) => { + console.log(err); + }); + }); + } + + revealView.style.visibility = "visible"; + + const revealStyleOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + errorTextStyles: { + base: { + color: "#f44336", + paddingLeft: "20px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records[0].tokens; + const revealContainer = skyflow.container(Skyflow.ContainerType.REVEAL); + const revealCardNumberElement = revealContainer.create({ + token: fieldsTokenData.card_number[0].token, + label: "Card Number", + ...revealStyleOptions, + }); + revealCardNumberElement.mount("#revealCardNumber"); + + const revealCardCvvElement = revealContainer.create({ + token: fieldsTokenData.cvv[0].token, + label: "CVV", + ...revealStyleOptions, + altText: "###", + }); + revealCardCvvElement.mount("#revealCvv"); + + const revealCardExpiryElement = revealContainer.create({ + token: fieldsTokenData.expiration_date[0].token, + label: "Card Expiry Date", + ...revealStyleOptions, + }); + revealCardExpiryElement.mount("#revealExpiryDate"); + + const revealCardholderNameElement = revealContainer.create({ + token: fieldsTokenData.name[0].token, + label: "Card Holder Name", + ...revealStyleOptions, + }); + revealCardholderNameElement.mount("#revealCardholderName"); + + const revealButton = document.getElementById("revealPCIData"); + + // update Reveal elements' properties + const updateRevealElementsButton = document.getElementById( + "updateRevealElements" + ); + if (updateRevealElementsButton) { + updateRevealElementsButton.addEventListener("click", () => { + // update label,inputStyles on cardholderName, + revealCardholderNameElement.update({ + label: "CARDHOLDER NAME", + inputStyles: { + base: { + color: "#aa11aa", + }, + }, + }); + + // update label,labelSyles on card number + revealCardNumberElement.update({ + label: "CARD NUMBER", + labelStyles: { + base: { + borderWidth: "5px", + }, + }, + }); + + // update inputStyles on expiry date + revealCardExpiryElement.update({ + inputStyles: { + base: { + backgroundColor: "#000", + color: "#fff", + }, + }, + }); + + // update altText,token,inputStyles,errorTextStyles on cvv + revealCardCvvElement.update({ + altText: "XXXX-XX", + token: "new-random-roken", + inputStyles: { + base: { + color: "#fff", + backgroundColor: "#000", + borderColor: "#f00", + borderWidth: "5px", + }, + }, + errorTextStyles: { + base: { + backgroundColor: "#000", + border: "1px #f00 solid", + }, + }, + }); + }); + } + + if (revealButton) { + revealButton.addEventListener("click", () => { + revealContainer + .reveal() + .then((res) => { + console.log(res); + }) + .catch((err) => { + console.log(err); + }); + }); + } +} catch (err) { + console.log(err); +} diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/package.json b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/package.json new file mode 100644 index 000000000..a8031ae4a --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflowelements", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "1.0.0-dev.2432cf4c" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-npm/skyflow-elements/src/index.html b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/src/index.html similarity index 100% rename from samples/using-npm/skyflow-elements/src/index.html rename to packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/src/index.js b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/src/index.js new file mode 100644 index 000000000..7ce8e9f1e --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/src/index.js @@ -0,0 +1,235 @@ +/* + Copyright (c) 2022 Skyflow, Inc. +*/ +import Skyflow from 'skyflow-flowvault-js'; + +try { + const revealView = document.getElementById('revealView'); + revealView.style.visibility = 'hidden'; + const skyflow = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + }); + + // Create collect Container. + const collectContainer = skyflow.container(Skyflow.ContainerType.COLLECT); + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + requiredAsterisk:{ + color: 'red' + } + }, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + }; + + // Create collect elements. + const cardNumberElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + },{ + required: true + }); + + const cvvElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'cvv', + ...collectStylesOptions, + label: 'Cvv', + placeholder: 'cvv', + type: Skyflow.ElementType.CVV, + }); + + const expiryDateElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + }); + + const cardHolderNameElement = collectContainer.create({ + tableName: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + }); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + cvvElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + cardHolderNameElement.mount('#collectCardholderName'); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData'); + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse = collectContainer.collect(); + collectResponse + .then((response) => { + document.getElementById('collectResponse').innerHTML = + JSON.stringify(response, null, 2); + + revealView.style.visibility = 'visible'; + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records[0].tokens; + const revealContainer = skyflow.container( + Skyflow.ContainerType.REVEAL + ); + const revealCardNumberElement = revealContainer.create({ + token: fieldsTokenData.card_number[0].token, + label: 'Card Number', + ...revealStyleOptions, + }); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealCardCvvElement = revealContainer.create({ + token: fieldsTokenData.cvv[0].token, + label: 'CVV', + ...revealStyleOptions, + altText: '###', + }); + revealCardCvvElement.mount('#revealCvv'); + + const revealCardExpiryElement = revealContainer.create({ + token: fieldsTokenData.expiry_date[0].token, + label: 'Card Expiry Date', + ...revealStyleOptions, + }); + revealCardExpiryElement.mount('#revealExpiryDate'); + + const revealCardholderNameElement = revealContainer.create({ + token: fieldsTokenData.first_name[0].token, + label: 'Card Holder Name', + ...revealStyleOptions, + }); + revealCardholderNameElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData'); + + if (revealButton) { + revealButton.addEventListener('click', () => { + revealContainer.reveal({ + tokenGroupRedactions: [ + { + tokenGroupName: 'deterministic', + redaction: 'redacted', + }, + ], + }).then((res) => { + console.log(res); + }).catch((err) => { + console.log(err); + }); + }); + } + }) + .catch((err) => { + console.log(err); + }); + }); + } +} catch (err) { + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/bearer-token-with-context.html b/packages/skyflow-flowvault-js/samples/using-script-tag/bearer-token-with-context.html new file mode 100644 index 000000000..1217e85a3 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/bearer-token-with-context.html @@ -0,0 +1,149 @@ + + + + + + + Bearer Token Generation with Context + + + + +

    Bearer Token Generation with Context

    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/collect-element-listeners.html b/packages/skyflow-flowvault-js/samples/using-script-tag/collect-element-listeners.html new file mode 100644 index 000000000..8ace785a7 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/collect-element-listeners.html @@ -0,0 +1,190 @@ + + + + + + + Collect Element Listeners + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/collect-elements-input-formatting.html b/packages/skyflow-flowvault-js/samples/using-script-tag/collect-elements-input-formatting.html new file mode 100644 index 000000000..7a627f216 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/collect-elements-input-formatting.html @@ -0,0 +1,180 @@ + + + + + + + + + Skyflow Elements Input Formatting + + + + +

    Collect Elements

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +    
    +
    + + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/collect-elements.html b/packages/skyflow-flowvault-js/samples/using-script-tag/collect-elements.html new file mode 100644 index 000000000..3f54a7467 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/collect-elements.html @@ -0,0 +1,145 @@ + + + + + + + Collect Element + + + + + +

    Collect Elements

    + + +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + + diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/composable-elements-update.html b/packages/skyflow-flowvault-js/samples/using-script-tag/composable-elements-update.html new file mode 100644 index 000000000..e27485d8d --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/composable-elements-update.html @@ -0,0 +1,384 @@ + + + + + + + + Skyflow Elements + + + + + +

    Composable Elements

    +
    +
    +
    + + +
    + +
    +
    
    +        
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/composable-elements.html b/packages/skyflow-flowvault-js/samples/using-script-tag/composable-elements.html new file mode 100644 index 000000000..7d1ee4a72 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/composable-elements.html @@ -0,0 +1,307 @@ + + + + + + + + Skyflow Elements + + + + + +

    Composable Elements

    +
    +
    +
    + +
    + +
    +
    
    +		
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/composable-reveal.html b/packages/skyflow-flowvault-js/samples/using-script-tag/composable-reveal.html new file mode 100644 index 000000000..aa2f8f398 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/composable-reveal.html @@ -0,0 +1,177 @@ + + + + + + + Skyflow Elements + + + + + +
    +

    Reveal Elements

    +
    + +
    +
    + + + + diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/custom-validations.html b/packages/skyflow-flowvault-js/samples/using-script-tag/custom-validations.html new file mode 100644 index 000000000..f6e609528 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/custom-validations.html @@ -0,0 +1,175 @@ + + + + + + + + Custom Validations + + + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    + + + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/masking.html b/packages/skyflow-flowvault-js/samples/using-script-tag/masking.html new file mode 100644 index 000000000..e002daa2c --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/masking.html @@ -0,0 +1,201 @@ + + + + + + + + Collect Element Listeners + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    
    +      
    +
    + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/reveal-elements-input-formatting.html b/packages/skyflow-flowvault-js/samples/using-script-tag/reveal-elements-input-formatting.html new file mode 100644 index 000000000..2fc9c6e23 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/reveal-elements-input-formatting.html @@ -0,0 +1,147 @@ + + + + + + + + + Skyflow Reveal Elements Input Formatting + + + + + +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    + + + + + + \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements-update-records.html b/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements-update-records.html new file mode 100644 index 000000000..b970b251f --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements-update-records.html @@ -0,0 +1,188 @@ + + + + + + + Skyflow Elements + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + + + diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements-update.html b/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements-update.html new file mode 100644 index 000000000..43c823790 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements-update.html @@ -0,0 +1,407 @@ + + + + + + + Skyflow Elements Update + + + + + +
    +

    Collect Elements

    +
    +
    +
    +
    +
    + + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + + +
    +
    + + + + + diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements.html b/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements.html new file mode 100644 index 000000000..286ba59c8 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/skyflow-elements.html @@ -0,0 +1,280 @@ + + + + + + + Skyflow Elements + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/upsert-support.html b/packages/skyflow-flowvault-js/samples/using-script-tag/upsert-support.html new file mode 100644 index 000000000..b98010b6e --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/upsert-support.html @@ -0,0 +1,228 @@ + + + + + + + + Skyflow Elements + + + + + +

    Collect Elements

    + +
    +
    +
    +
    + +
    +
    +
    
    +    
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    + +
    +
    + + + + + + \ No newline at end of file diff --git a/samples/using-typescript/README.md b/packages/skyflow-flowvault-js/samples/using-typescript/README.md similarity index 100% rename from samples/using-typescript/README.md rename to packages/skyflow-flowvault-js/samples/using-typescript/README.md diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/package.json new file mode 100644 index 000000000..797e15226 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflow-reveal-composable-elements", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/Reveal-composable/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/src/index.html similarity index 100% rename from samples/using-typescript/Reveal-composable/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/src/index.ts new file mode 100644 index 000000000..680862ee1 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/Reveal-composable/src/index.ts @@ -0,0 +1,162 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + ComposableRevealContainer, + ErrorTextStyles, + InputStyles, + LabelStyles, + RevealElementInput, + RevealOptions, + RevealResponse, + SkyflowConfig, + ComposableRevealElement, + SkyflowError, +} from 'skyflow-flowvault-js'; + +try { + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + } + const skyflowClient: Skyflow = Skyflow.init(config); + + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as ErrorTextStyles, + }; + const revealContainerOptions = { + layout: [1, 1, 1, 1], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '30px 16px', + borderRadius: '4px', + margin: '12px 2px', + boxShadow: '8px', + width: '400px', + } + }, + errorTextStyles: { + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } + } + + const revealContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, revealContainerOptions) as ComposableRevealContainer;; + + + const revealCardNumberInput: RevealElementInput = { + token: "", + label: 'Card Number', + ...revealStyleOptions, + } + const revealCardNumberElement: ComposableRevealElement = revealContainer.create(revealCardNumberInput); + + const revealCardCvvInput: RevealElementInput = { + token: "", + label: 'CVV', + ...revealStyleOptions, + altText: '###', + } + const revealCardCvvElement: ComposableRevealElement = revealContainer.create(revealCardCvvInput); + + const revealCardExpiryInput: RevealElementInput = { + token: "", + label: 'Card Expiry Date', + ...revealStyleOptions, + } + const revealCardExpiryElement: ComposableRevealElement = revealContainer.create(revealCardExpiryInput); + + const revealCardholderNameInput: RevealElementInput = { + token: "", + label: 'Card Holder Name', + ...revealStyleOptions, + } + const revealCardholderNameElement: ComposableRevealElement = revealContainer.create(revealCardholderNameInput); + + revealContainer.mount(document.getElementById('revealComposableContainer') as HTMLElement); + + const revealButton = document.getElementById('revealPCIData') as HTMLButtonElement; + + if (revealButton) { + revealButton.addEventListener('click', () => { + // Redaction is applied per token group via reveal options. + const revealOptions: RevealOptions = { + tokenGroupRedactions: [ + { + tokenGroupName: 'deterministic', + redaction: 'redacted', + }, + { + tokenGroupName: 'non_deterministic', + redaction: 'mask1', // custom redaction mask + }, + ], + }; + const revealResponse: Promise = revealContainer.reveal(revealOptions) + revealResponse.then((res: RevealResponse) => { + console.log(res); + }).catch((err: SkyflowError) => { + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/package.json new file mode 100644 index 000000000..307a1ab44 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/package.json @@ -0,0 +1,16 @@ +{ + "name": "collectelementlisteners", + "version": "1.0.0", + "description": "A Sample on how to add event listeners on Collect Elements ", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/collect-element-listeners/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/src/index.html similarity index 100% rename from samples/using-typescript/collect-element-listeners/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/src/index.ts new file mode 100644 index 000000000..7997fab15 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/collect-element-listeners/src/index.ts @@ -0,0 +1,183 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ + +import Skyflow, { + CollectContainer, + CollectElement, + CollectResponse, + ErrorTextStyles, + InputStyles, + SkyflowConfig, + LabelStyles, + CollectElementInput, + ElementState, + SkyflowError, +} from 'skyflow-flowvault-js'; + +try { + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + // Actual value of element can only be accessed inside the handler, + // when the env is set to DEV. + // Make sure the env is set to PROD when using skyflow-flowvault-js in production + env: Skyflow.Env.DEV, + } + } + const skyflowClient: Skyflow = Skyflow.init(config); + + // Create collect Container + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + + // Custom styles for collect elements + const inputStyles: InputStyles = { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + "&:hover": { +      borderColor: "green", +    }, + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + } + const labelStyles: LabelStyles = { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + } + const errorTextStyles: ErrorTextStyles = { + base: { + color: '#f44336', + }, + } + + // Create collect elements + const cardNumberInput : CollectElementInput = { + tableName: 'pii_fields', + column: 'card_number', + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + }; + const cardNumberElement: CollectElement = collectContainer.create(cardNumberInput); + + const cvvInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'cvv', + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: 'Cvv', + placeholder: 'cvv', + type: Skyflow.ElementType.CVV, + }; + const cvvElement: CollectElement = collectContainer.create(cvvInput); + + const expiryDateInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'expiry_date', + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + }; + const expiryDateElement: CollectElement = collectContainer.create(expiryDateInput); + + const cardHolderNameInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'first_name', + inputStyles: inputStyles, + labelStyles: labelStyles, + errorTextStyles: errorTextStyles, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + }; + const cardHolderNameElement: CollectElement = collectContainer.create(cardHolderNameInput); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + cvvElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + cardHolderNameElement.mount('#collectCardholderName'); + + // Add listeners to Collect Elements. + + // Add READY EVENT Listener. + cardNumberElement.on(Skyflow.EventName.READY, (readyState: ElementState) => { + console.log('Ready Event Triggered', readyState); + }); + + // Add CHANGE EVENT Listener. + cvvElement.on(Skyflow.EventName.CHANGE, (changeState: ElementState) => { + console.log('CHANGE Event Triggered', changeState); + }); + + // Add FOCUS EVENT Listener. + expiryDateElement.on(Skyflow.EventName.FOCUS, (focusState: ElementState) => { + console.log('FOCUS Event Triggered', focusState); + }); + + // Add BLUR EVENT Listener. + cardHolderNameElement.on(Skyflow.EventName.BLUR, (blurState: ElementState) => { + console.log('BLUR Event Triggered', blurState); + }); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData') as HTMLButtonElement; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse: Promise = collectContainer.collect(); + collectResponse + .then((response: CollectResponse) => { + console.log(response); + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + }) + .catch((err: SkyflowError) => { + console.log(err); + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(err, null, 2); + } + }); + }); + } +} catch (err: unknown) { + console.error(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/package.json new file mode 100644 index 000000000..64a69d3f8 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/package.json @@ -0,0 +1,18 @@ +{ + "name": "composable-elements-update", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.8.3" + } +} diff --git a/samples/using-typescript/composable-elements-update/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/src/index.html similarity index 100% rename from samples/using-typescript/composable-elements-update/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/src/index.ts new file mode 100644 index 000000000..b743ed3bb --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements-update/src/index.ts @@ -0,0 +1,360 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ + +import Skyflow, { + CollectElementInput, + CollectResponse, + ComposableContainer, + ComposableElement, + ContainerOptions, + ErrorTextStyles, + SkyflowConfig, + InputStyles, + RevealElementInput, + ValidationRule, + LabelStyles, + RevealContainer, + RevealElement, + RevealResponse, + CollectElementUpdateOptions, + SkyflowError, +} from 'skyflow-flowvault-js'; + +try { + const revealView = document.getElementById('revealView') as HTMLElement; + if (revealView) { + revealView.style.visibility = 'hidden'; + } + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + } + const skyflowClient: Skyflow = Skyflow.init(config); + + //custom styles for collect elements + const cardholderStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '294px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + errorTextStyles: { + } as ErrorTextStyles, + }; + + const cardNumberStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '294px', + paddingLeft: '18px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + errorTextStyles: { + } as ErrorTextStyles, + }; + + const expiryDateStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '49px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + errorTextStyles: { + } as ErrorTextStyles, + }; + + const cvvStyles = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '30px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + errorTextStyles: { + base: { + color: 'red' + } + } as ErrorTextStyles, + }; + + const containerOptions: ContainerOptions = { + layout: [1, 3], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + boxShadow: '8px' + } + } as InputStyles, + errorTextStyles: { + base: { + color: 'red' + } + } as ErrorTextStyles, + } + // create collect Container + const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions) as ComposableContainer; + + const cardHolderNameInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'first_name', + ...cardholderStyles, + label: 'Cardholder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + } + const cardHolderNameElement: ComposableElement = composableContainer.create(cardHolderNameInput); + + const cardNumberInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'card_number', + ...cardNumberStyles, + type: Skyflow.ElementType.CARD_NUMBER, + placeholder: 'XXXX XXXX XXXX XXXX' + } + const cardNumberElement: ComposableElement = composableContainer.create(cardNumberInput); + + const expiryDateInput: CollectElementInput = { + tableName: 'cards', + column: 'expiry_date', + ...expiryDateStyles, + placeholder: 'MM/YY', + type: Skyflow.ElementType.EXPIRATION_DATE, + } + const expiryDateElement: ComposableElement = composableContainer.create(expiryDateInput); + + const cvvInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'cvv', + ...cvvStyles, + placeholder: 'CVC', + type: Skyflow.ElementType.CVV, + } + const cvvElement: ComposableElement = composableContainer.create(cvvInput); + + // mount the container + composableContainer.mount('#composableContainer'); + + // Add OnSubmit event listner on composable container + composableContainer.on(Skyflow.EventName.SUBMIT, () => { + // Handle when enter key pressed in any container elements + console.log('Submit Listener is being Triggered.'); + }); + + + // Sample helper function to determine cvv length. + const findCvvLength = (cardBinValue: string): number => { + const amexRegex = /^3[47][0-9]{4}$/ + return amexRegex.test(cardBinValue.slice(0, 6)) ? 4 : 3 + }; + + // Validation rules for cvv element. + const length3Rule: ValidationRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: 'cvv must be 3 digits', + }, + }; + + const length4Rule: ValidationRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 4, + error: 'cvv must be 4 digits', + }, + }; + + // OnChange listener for cardNumber element. + cardNumberElement.on(Skyflow.EventName.CHANGE, (state: any) => { + console.log('update validation', state) + if (state.isValid && state.value) { + // update cvv element validation rule. + if (findCvvLength(state.value) === 3) { + const updateOptions: CollectElementUpdateOptions = { validations: [length3Rule] } + cvvElement.update(updateOptions); + } + else { + const updateOptions: CollectElementUpdateOptions = { validations: [length4Rule] } + cvvElement.update(updateOptions); + } + } + }); + + // update composable elements + const updateElementsButton = document.getElementById('updateElements') as HTMLButtonElement; + if (updateElementsButton) { + updateElementsButton.addEventListener('click', () => { + // update label,placeholder on cardholderName, + cardHolderNameElement.update({ + label: 'CARDHOLDER NAME', + placeholder: 'Eg: John' + } as CollectElementUpdateOptions); + + // update styles on card number + cardNumberElement.update({ + inputStyles: { + base: { + color: 'blue' + } + } + } as CollectElementUpdateOptions); + + // update table,coloumn on expiry date + expiryDateElement.update({ + tableName: 'pii_fields', + column: 'expiry_date', + } as CollectElementUpdateOptions); + + }); + } + + // collect all elements data + const collectButton = document.getElementById('collectPCIData') as HTMLButtonElement; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse: Promise = composableContainer.collect(); + collectResponse + .then((response: CollectResponse) => { + console.log(response); + response = response; + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + + if (revealView) { + revealView.style.visibility = 'visible'; + } + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + }, + } as ErrorTextStyles, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records![0].tokens!; + const revealContainer = skyflowClient.container( + Skyflow.ContainerType.REVEAL + ) as RevealContainer; + const revealCardNumberInput: RevealElementInput = { + token: fieldsTokenData.card_number[0].token, + label: 'Card Number', + ...revealStyleOptions, + }; + const revealCardNumberElement: RevealElement = revealContainer.create(revealCardNumberInput); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealCardCvvInput: RevealElementInput = { + token: fieldsTokenData.cvv[0].token, + label: 'Cvv', + ...revealStyleOptions, + }; + const revealCardCvvElement: RevealElement = revealContainer.create(revealCardCvvInput); + revealCardCvvElement.mount('#revealCvv'); + + const revealCardExpiryInput: RevealElementInput = { + token: fieldsTokenData.expiry_date[0].token, + label: 'Card Expiry Date', + ...revealStyleOptions, + }; + const revealCardExpiryElement: RevealElement = revealContainer.create(revealCardExpiryInput); + revealCardExpiryElement.mount('#revealExpiryDate'); + + const revealCardholderNameInput: RevealElementInput = { + token: fieldsTokenData.first_name[0].token, + label: 'Card Holder Name', + ...revealStyleOptions, + }; + const revealCardholderNameElement: RevealElement = revealContainer.create(revealCardholderNameInput); + revealCardholderNameElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData') as HTMLButtonElement; + + if (revealButton) { + revealButton.addEventListener('click', () => { + const revealResonse: Promise = revealContainer.reveal(); + revealResonse.then((res: RevealResponse) => { + console.log(res); + }) + .catch((err: SkyflowError) => { + console.log(err); + }); + }); + } + }) + .catch((err: SkyflowError) => { + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.error(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/package.json new file mode 100644 index 000000000..594c54f4d --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/package.json @@ -0,0 +1,16 @@ +{ + "name": "composableelements", + "version": "1.0.0", + "description": "A Sample on how to add Composable Elements ", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/composable-elements/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/src/index.html similarity index 100% rename from samples/using-typescript/composable-elements/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/src/index.ts new file mode 100644 index 000000000..47b93b9b8 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/composable-elements/src/index.ts @@ -0,0 +1,291 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ + +import Skyflow, { + ComposableContainer, + ComposableElement, + CollectElementInput, + CollectResponse, + RevealContainer, + RevealElement, + RevealResponse, + SkyflowConfig, + InputStyles, + LabelStyles, + ContainerOptions, + ErrorTextStyles, + RevealElementInput, + SkyflowError, +} from 'skyflow-flowvault-js'; + +try { + const revealView = document.getElementById('revealView') as HTMLElement; + if (revealView) { + revealView.style.visibility = 'hidden'; + } + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + } + const skyflowClient: Skyflow = Skyflow.init(config); + + //custom styles for collect elements + const cardholderStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '294px', + "&:hover": { +      borderColor: "green", +     }, + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as InputStyles, + labelStyles: { + } as LabelStyles, + }; + + const cardNumberStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '294px', + paddingLeft: '18px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + }; + + const expiryDateStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '49px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + }; + + const cvvStyles = { + inputStyles: { + base: { + fontFamily: '"Roboto", sans-serif', + fontStyle: 'normal', + fontWeight: '400', + fontSize: '14px', + lineHeight: '21px', + width: '30px' + }, + } as InputStyles, + labelStyles: { + } as LabelStyles, + }; + + const containerOptions: ContainerOptions = { + layout: [1, 3], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + boxShadow: '8px' + } + } as InputStyles, + errorTextStyles: { + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as ErrorTextStyles, + } + // create collect Container + const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions) as ComposableContainer; + + const cardHolderNameInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'first_name', + ...cardholderStyles, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + } + const cardHolderNameElement: ComposableElement = composableContainer.create(cardHolderNameInput); + + const cardNumberInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'card_number', + ...cardNumberStyles, + type: Skyflow.ElementType.CARD_NUMBER, + placeholder: 'XXXX XXXX XXXX XXXX' + } + const cardNumberElement: ComposableElement = composableContainer.create(cardNumberInput); + + const expiryDateInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'expiry_date', + ...expiryDateStyles, + placeholder: 'MM/YY', + type: Skyflow.ElementType.EXPIRATION_DATE, + } + const expiryDateElement: ComposableElement = composableContainer.create(expiryDateInput); + + const cvvInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'cvv', + ...cvvStyles, + placeholder: 'CVC', + type: Skyflow.ElementType.CVV, + } + const cvvElement: ComposableElement = composableContainer.create(cvvInput); + + // mount the container + composableContainer.mount('#composableContainer'); + + // collect all elements data + const collectButton = document.getElementById('collectPCIData') as HTMLButtonElement; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse: Promise = composableContainer.collect(); + collectResponse + .then((response: CollectResponse) => { + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + + if (revealView) { + revealView.style.visibility = 'visible'; + } + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + } as ErrorTextStyles, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records![0].tokens!; + const revealContainer = skyflowClient.container( + Skyflow.ContainerType.REVEAL + ) as RevealContainer; + + const revealCardNumberInput: RevealElementInput = { + token: fieldsTokenData.card_number[0].token, + label: 'Card Number', + ...revealStyleOptions, + }; + const revealCardNumberElement: RevealElement = revealContainer.create(revealCardNumberInput); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealCardCvvInput: RevealElementInput = { + token: fieldsTokenData.cvv[0].token, + label: 'Cvv', + ...revealStyleOptions, + } + const revealCardCvvElement: RevealElement = revealContainer.create(revealCardCvvInput); + revealCardCvvElement.mount('#revealCvv'); + + const revealCardExpiryInput: RevealElementInput = { + token: fieldsTokenData.expiry_date[0].token, + label: 'Card Expiry Date', + ...revealStyleOptions, + }; + const revealCardExpiryElement: RevealElement = revealContainer.create(revealCardExpiryInput); + revealCardExpiryElement.mount('#revealExpiryDate'); + + const revealCardholderNameInput: RevealElementInput = { + token: fieldsTokenData.first_name[0].token, + label: 'Card Holder Name', + ...revealStyleOptions, + } + const revealCardholderNameElement: RevealElement = revealContainer.create(revealCardholderNameInput); + revealCardholderNameElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData') as HTMLButtonElement; + + if (revealButton) { + revealButton.addEventListener('click', () => { + const revealResponse: Promise = revealContainer.reveal(); + revealResponse.then((res: RevealResponse) => { + console.log(res); + }) + .catch((err: SkyflowError) => { + console.error(err); + }); + }); + } + }) + .catch((err: SkyflowError) => { + console.log(err); + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(err, null, 2); + } + }); + }); + } +} catch (err: unknown) { + console.error(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/package.json new file mode 100644 index 000000000..1779b6213 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/package.json @@ -0,0 +1,17 @@ +{ + "name": "customvalidations", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open --no-cache", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/custom-validations/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/src/index.html similarity index 100% rename from samples/using-typescript/custom-validations/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/src/index.ts new file mode 100644 index 000000000..168fa8ed4 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/custom-validations/src/index.ts @@ -0,0 +1,152 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + CollectContainer, + CollectElement, + CollectElementInput, + ErrorTextStyles, + SkyflowConfig, + InputStyles, + ValidationRule, + LabelStyles, +} from 'skyflow-flowvault-js'; + +try{ + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options:{ + logLevel:Skyflow.LogLevel.ERROR, + env:Skyflow.Env.PROD, + } + } + const skyflowClient: Skyflow = Skyflow.init(config); + + // Create collect Container. + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + }, + } as ErrorTextStyles, + }; + + // Create a validation rule. + const regexRule: ValidationRule = { + // REGEX Rule will validate the element value with the given regex + type:Skyflow.ValidationRuleType.REGEX_MATCH_RULE , + params:{ + // regex rule expects a regex to be tested on element value + regex:/[A-Za-z0-9]+/, + // specify what error text should be displayed + // when this validation rule failed + error:'only alphabets are allowed' + } + } + // Creating a length rule. + const lengthRule: ValidationRule = { + // LENGTH match rule will validate whether the element value length matches with given length. + type:Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params:{ + // specify minimum length that element value should have + min:3, + // specify maximum length that element value should have + max:12, + // specify what error text should be displayed + // when this validation rule failed + error:'must be between 3 to 12 alphabets' + } + } + + const userNameInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + placeholder: 'Enter User Name', + label: 'User Name', + type: Skyflow.ElementType.INPUT_FIELD, + // pass validation rules + validations:[regexRule,lengthRule] + } + const userNameElement: CollectElement = collectContainer.create(userNameInput); + + const passwordInput: CollectElementInput = { + ...collectStylesOptions, + label: 'Enter Password', + placeholder: 'Password', + type: Skyflow.ElementType.INPUT_FIELD, + } + const passwordElement: CollectElement = collectContainer.create(passwordInput); + + const elementMatchRule: ValidationRule = { + // ELEMENT VALUE MATCH RULE validates that element value matches the provied element. + type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + // Specify with which element value should be matched. + element: passwordElement, + // Specify what error text should be displayed + // when this validation rule failed + error: 'password doesn’t match' + } + } + + const confirmPasswordInput: CollectElementInput = { + ...collectStylesOptions, + label: 'Confirm Password', + placeholder: 'confirm password', + type: Skyflow.ElementType.INPUT_FIELD, + // Add validations. + validations:[elementMatchRule] + } + const confirmPasswordElement: CollectElement = collectContainer.create(confirmPasswordInput); + + // Mount the elements. + userNameElement.mount('#collectUserName'); + passwordElement.mount('#collectPassword'); + confirmPasswordElement.mount('#collectConfirmPassword'); + +} catch (err: unknown) { + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/package.json new file mode 100644 index 000000000..89ad4064a --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflowelements", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts new file mode 100644 index 000000000..ab5554ba7 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts @@ -0,0 +1,180 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + CollectContainer, + CollectElement, + CollectElementInput, + CollectElementOptions, + CollectResponse, + ErrorTextStyles, + InputStyles, + SkyflowConfig, + LabelStyles, + SkyflowError, +} from "skyflow-flowvault-js"; + +try { + + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + } + const skyflowClient: Skyflow = Skyflow.init(config); + + // Create collect Container. + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + }, + } as ErrorTextStyles, + }; + + // Create collect elements. + const cardNumberInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + }; + const cardNumberOptions: CollectElementOptions = { + required: false, + format: 'XXXX-XXXX-XXXX-XXXX' // inbuilt format + }; + const cardNumberElement: CollectElement = collectContainer.create( + cardNumberInput, + cardNumberOptions + ); + + const ssnInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'ssn', + ...collectStylesOptions, + label: 'SSN', + placeholder: 'ssn', + type: Skyflow.ElementType.INPUT_FIELD, + }; + const ssnOptions: CollectElementOptions = { + required: false, + format: 'XXX-XX-XXXX', + translation: { X: '[0-9]' } // translates each 'X' in format string accepts a digit ranging from 0-9. + }; + const ssnElement: CollectElement = collectContainer.create(ssnInput, ssnOptions); + + const expiryDateInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + }; + const expiryDateOptions: CollectElementOptions = { + required: false, + format: 'MM/YYYY' // inbuilt format. + }; + const expiryDateElement: CollectElement = collectContainer.create( + expiryDateInput, + expiryDateOptions, + ); + + const passportNumberInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'passport_number', + ...collectStylesOptions, + label: 'Passport Number', + placeholder: 'passport number', + type: Skyflow.ElementType.INPUT_FIELD, + }; + const passportNumberOptions: CollectElementOptions = { + required: false, + format: 'XXYYYYYYY', + translation: { X: '[A-Z]', Y: '[0-9]' } + // translates each 'X' in format string accepts a uppercase alphabet A to Z. + // and each 'Y' in format string accepts a digit ranging from 0-9. + }; + const passportNumberElement: CollectElement = collectContainer.create( + passportNumberInput, + passportNumberOptions, + ); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + ssnElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + passportNumberElement.mount('#collectCardholderName'); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData') as HTMLButtonElement; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse: Promise = collectContainer.collect(); + collectResponse + .then((response: CollectResponse) => { + console.log(response); + response = response; + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + }) + .catch((err: SkyflowError) => { + const errorElement = document.getElementById('collectResponse') as HTMLElement; + if (errorElement){ + errorElement.innerHTML = JSON.stringify(err, null, 2); + } + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.log(err); +} \ No newline at end of file diff --git a/samples/using-typescript/skyflow-elements-input-formatting/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/index.html similarity index 100% rename from samples/using-typescript/skyflow-elements-input-formatting/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts new file mode 100644 index 000000000..b24dbeeee --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts @@ -0,0 +1,141 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + ErrorTextStyles, + InputStyles, + RevealElementOptions, + RevealElementInput, + SkyflowConfig, + LabelStyles, + RevealContainer, + RevealElement, + RevealResponse, + SkyflowError, +} from "skyflow-flowvault-js"; + +try { + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + } + const skyflowClient: Skyflow = Skyflow.init(config); + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + }, + } as ErrorTextStyles, + }; + + const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL) as RevealContainer; + const revealCardNumberInput: RevealElementInput = { + token: '', + label: 'Card Number', + ...revealStyleOptions, + }; + const revealCardNumberOptions: RevealElementOptions = { + format: 'XXXX-XXXX-XXXX-XXXX', + translation: { X: '[0-9]' } + }; + const revealCardNumberElement: RevealElement = revealContainer.create( + revealCardNumberInput, + revealCardNumberOptions, + ); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealSSNInput: RevealElementInput = { + token: '', + label: 'SSN', + ...revealStyleOptions, + altText: '###', + }; + const revealSSNOptions: RevealElementOptions = { + format: 'XX-XXX-XXXX', + }; + const revealSSNElement: RevealElement = revealContainer.create( + revealSSNInput, + revealSSNOptions + ); + revealSSNElement.mount('#revealCvv'); + + const revealPhoneNumberInput: RevealElementInput = { + token: '', + label: 'Phone Number', + ...revealStyleOptions, + } + const revealPhoneNumberOptions: RevealElementOptions = { + format: '(XXX) XXX-XXXX', + translation: { X: '[0-9]' } + } + const revealPhoneNumberElement: RevealElement = revealContainer.create( + revealPhoneNumberInput, + revealPhoneNumberOptions, + ); + revealPhoneNumberElement.mount('#revealExpiryDate'); + + const revealDrivingLicenseInput: RevealElementInput = { + token: '', + label: 'Driving License', + ...revealStyleOptions, + }; + const revealDrivingLicenseOptions: RevealElementOptions = { + format: 'YXX XXXX XXXX', + translation: { Y: '[A-Z]', X: '[0-9]' } + } + const revealDrivingLicenseElement: RevealElement = revealContainer.create( + revealDrivingLicenseInput, + revealDrivingLicenseOptions + ); + revealDrivingLicenseElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData') as HTMLButtonElement; + + if (revealButton) { + revealButton.addEventListener('click', () => { + const revealResponse: Promise = revealContainer.reveal(); + revealResponse.then((res: RevealResponse) => { + console.log(res); + }).catch((err: SkyflowError) => { + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.log(err); +} diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/package.json new file mode 100644 index 000000000..db2c41779 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflow-elements-update-records", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/skyflow-elements-update-records/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/src/index.html similarity index 100% rename from samples/using-typescript/skyflow-elements-update-records/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts new file mode 100644 index 000000000..008094f7a --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts @@ -0,0 +1,174 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + CollectContainer, + CollectElement, + CollectElementInput, + CollectResponse, + ErrorTextStyles, + CollectOptions, + AdditionalFields, + AdditionalFieldsRecord, + InputStyles, + LabelStyles, + SkyflowConfig, + SkyflowError, +} from 'skyflow-flowvault-js'; + +try { + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + } + const skyflowClient: Skyflow = Skyflow.init(config); + // Create collect Container. + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + }, + } as ErrorTextStyles, + }; + + // Create collect elements. + const cardNumberInput: CollectElementInput = { + tableName: 'table1', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'card number', + label: 'Card Number', + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + type: Skyflow.ElementType.CARD_NUMBER, + }; + const cardNumberElement: CollectElement = collectContainer.create(cardNumberInput); + + const cvvInput: CollectElementInput = { + tableName: 'table1', + column: 'cvv', + ...collectStylesOptions, + label: 'Cvv', + placeholder: 'cvv', + type: Skyflow.ElementType.CVV, + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + }; + const cvvElement: CollectElement = collectContainer.create(cvvInput); + + const expiryDateInput: CollectElementInput = { + tableName: 'table1', + column: 'expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + }; + const expiryDateElement: CollectElement = collectContainer.create(expiryDateInput); + + const cardHolderNameInput: CollectElementInput = { + tableName: 'table2', + column: 'name', + ...collectStylesOptions, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + }; + const cardHolderNameElement: CollectElement = collectContainer.create(cardHolderNameInput); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + cvvElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + cardHolderNameElement.mount('#collectCardholderName'); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData') as HTMLButtonElement; + const records: Array = [ + { + tableName: 'table1', + data: { + gender: 'MALE', + }, + skyflowId: '', // Replace with a valid Skyflow ID of the record to update + }, + { + tableName: 'table2', + data: { + gender: 'MALE', + }, + }, + ]; + const additionalFields: AdditionalFields = { + records: records, + }; + const collectOptions: CollectOptions = { + additionalFields: additionalFields, + }; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse: Promise = collectContainer.collect(collectOptions); + collectResponse + .then((response: CollectResponse) => { + console.log(response); + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + }) + .catch((err: SkyflowError) => { + const errorElement = document.getElementById('collectResponse') as HTMLElement; + if (errorElement){ + errorElement.innerHTML = JSON.stringify(err, null, 2); + } + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.log(err); +} diff --git a/samples/using-npm/skyflow-elements-update/.gitignore b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/.gitignore similarity index 100% rename from samples/using-npm/skyflow-elements-update/.gitignore rename to packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/.gitignore diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/package.json new file mode 100644 index 000000000..642ded5c3 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/package.json @@ -0,0 +1,18 @@ +{ + "name": "skyflow-elements-update", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/skyflow-elements-update/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/src/index.html similarity index 100% rename from samples/using-typescript/skyflow-elements-update/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/src/index.ts new file mode 100644 index 000000000..863cd2334 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements-update/src/index.ts @@ -0,0 +1,430 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + CollectContainer, + CollectElement, + CollectElementInput, + CollectElementOptions, + CollectElementUpdateOptions, + CollectResponse, + ErrorTextStyles, + ElementState, + InputStyles, + LabelStyles, + RevealContainer, + RevealElement, + RevealElementInput, + RevealResponse, + SkyflowConfig, + ValidationRule, + SkyflowError, +} from "skyflow-flowvault-js"; + +try { + const revealView = document.getElementById("revealView") as HTMLElement; + if (revealView) { + revealView.style.visibility = "hidden"; + } + let collectResponseData: CollectResponse = { records: [] }; + const config: SkyflowConfig = { + vaultID: "", + vaultURL: "", + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ""; + Http.open("GET", url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + }, + }; + const skyflowClient: Skyflow = Skyflow.init(config); + + // Create collect Container. + const collectContainer = skyflowClient.container( + Skyflow.ContainerType.COLLECT + ) as CollectContainer; + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px", + fontFamily: '"Roboto", sans-serif', + }, + complete: { + color: "#4caf50", + }, + empty: {}, + focus: {}, + invalid: { + color: "#f44336", + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + requiredAsterisk: { + color: "red", + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: "#f44336", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as ErrorTextStyles, + }; + + // Create collect elements. + const cardNumberInput: CollectElementInput = { + tableName: "pii_fields", + column: "card_number", + ...collectStylesOptions, + placeholder: "card number", + label: "Card Number", + type: Skyflow.ElementType.CARD_NUMBER, + }; + const cardNumberOptions: CollectElementOptions = { + required: true, + }; + const cardNumberElement: CollectElement = collectContainer.create( + cardNumberInput, + cardNumberOptions + ); + + const cvvInput: CollectElementInput = { + tableName: "pii_fields", + column: "cvv", + ...collectStylesOptions, + label: "Cvv", + placeholder: "cvv", + type: Skyflow.ElementType.CVV, + }; + const cvvElement: CollectElement = collectContainer.create(cvvInput); + + const expiryDateInput: CollectElementInput = { + tableName: "pii_fields", + column: "expiry_date", + ...collectStylesOptions, + label: "Expiry Date", + placeholder: "MM/YYYY", + type: Skyflow.ElementType.EXPIRATION_DATE, + }; + const expiryDateElement: CollectElement = + collectContainer.create(expiryDateInput); + + const cardholderNameInput: CollectElementInput = { + tableName: "pii_fields", + column: "name", + ...collectStylesOptions, + label: "Card Holder Name", + placeholder: "cardholder name", + type: Skyflow.ElementType.CARDHOLDER_NAME, + }; + const cardHolderNameElement: CollectElement = + collectContainer.create(cardholderNameInput); + + // Mount the elements. + cardNumberElement.mount("#collectCardNumber"); + cvvElement.mount("#collectCvv"); + expiryDateElement.mount("#collectExpiryDate"); + cardHolderNameElement.mount("#collectCardholderName"); + + // Sample helper function to determine cvv length. + const findCvvLength = (cardBinValue: string) => { + const amexRegex = /^3[78][0-9]{4}$/; + return amexRegex.test(cardBinValue.slice(0, 6)) ? 4 : 3; + }; + + // Validation rules for cvv element. + const length3Rule: ValidationRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: "cvv must be 3 digits", + }, + }; + + const length4Rule: ValidationRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 4, + error: "cvv must be 4 digits", + }, + }; + + // OnChange listener for cardNumber element. + cardNumberElement.on(Skyflow.EventName.CHANGE, (state: ElementState) => { + if (state.isValid) { + // update cvv element validation rule. + if (findCvvLength(state.value as string) === 3) { + const updateOptions: CollectElementUpdateOptions = { + validations: [length3Rule], + }; + cvvElement.update(updateOptions); + } else { + const updateOptions: CollectElementUpdateOptions = { + validations: [length4Rule], + }; + cvvElement.update(updateOptions); + } + } + }); + + // update collect elements' properties + const updateCollectElementsButton = document.getElementById( + "updateCollectElements" + ) as HTMLButtonElement; + if (updateCollectElementsButton) { + updateCollectElementsButton.addEventListener("click", () => { + // update label,placeholder on cardholderName, + cardHolderNameElement.update({ + label: "CARDHOLDER NAME", + placeholder: "Eg: John", + type: Skyflow.ElementType.PIN, + } as CollectElementInput); + + // update styles on card number + cardNumberElement.update({ + inputStyles: { + base: { + color: "blue", + }, + }, + } as CollectElementUpdateOptions); + + // update table,coloumn on expiry date + expiryDateElement.update({ + tableName: "pii_fields", + column: "expiration_date", + } as CollectElementUpdateOptions); + }); + } + + // Collect all elements data. + const collectButton = document.getElementById( + "collectPCIData" + ) as HTMLButtonElement; + if (collectButton) { + collectButton.addEventListener("click", () => { + const collectResponse: Promise = + collectContainer.collect(); + collectResponse + .then((response: CollectResponse) => { + console.log(response); + collectResponseData = response; + const responseElement = document.getElementById( + "collectResponse" + ) as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + + revealView.style.visibility = "visible"; + + const revealStyleOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: "#f44336", + paddingLeft: "20px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as ErrorTextStyles, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = collectResponseData.records![0].tokens!; + const revealContainer = skyflowClient.container( + Skyflow.ContainerType.REVEAL + ) as RevealContainer; + + const revealCardNumberInput: RevealElementInput = { + token: fieldsTokenData.card_number[0].token, + label: "Card Number", + ...revealStyleOptions, + }; + const revealCardNumberElement: RevealElement = revealContainer.create( + revealCardNumberInput + ); + revealCardNumberElement.mount("#revealCardNumber"); + + const revealCardCvvInput: RevealElementInput = { + token: fieldsTokenData.cvv[0].token, + label: "CVV", + ...revealStyleOptions, + altText: "###", + }; + const revealCardCvvElement: RevealElement = + revealContainer.create(revealCardCvvInput); + revealCardCvvElement.mount("#revealCvv"); + + const revealCardExpiryInput: RevealElementInput = { + token: fieldsTokenData.expiration_date[0].token, + label: "Card Expiry Date", + ...revealStyleOptions, + }; + const revealCardExpiryElement: RevealElement = revealContainer.create( + revealCardExpiryInput + ); + revealCardExpiryElement.mount("#revealExpiryDate"); + + const revealCardholderNameInput: RevealElementInput = { + token: fieldsTokenData.name[0].token, + label: "Card Holder Name", + ...revealStyleOptions, + }; + const revealCardholderNameElement: RevealElement = + revealContainer.create(revealCardholderNameInput); + revealCardholderNameElement.mount("#revealCardholderName"); + + const revealButton = document.getElementById( + "revealPCIData" + ) as HTMLButtonElement; + + // update Reveal elements' properties + const updateRevealElementsButton = document.getElementById( + "updateRevealElements" + ) as HTMLButtonElement; + if (updateRevealElementsButton) { + updateRevealElementsButton.addEventListener("click", () => { + // update label,inputStyles on cardholderName, + revealCardholderNameElement.update({ + label: "CARDHOLDER NAME", + inputStyles: { + base: { + color: "#aa11aa", + }, + }, + } as RevealElementInput); + + // update label,labelSyles on card number + revealCardNumberElement.update({ + label: "CARD NUMBER", + labelStyles: { + base: { + borderWidth: "5px", + }, + }, + } as RevealElementInput); + + // update inputStyles on expiry date + revealCardExpiryElement.update({ + inputStyles: { + base: { + backgroundColor: "#000", + color: "#fff", + }, + }, + } as RevealElementInput); + + // update altText,token,inputStyles,errorTextStyles on cvv + revealCardCvvElement.update({ + altText: "XXXX-XX", + token: "new-random-roken", + inputStyles: { + base: { + color: "#fff", + backgroundColor: "#000", + borderColor: "#f00", + borderWidth: "5px", + }, + }, + errorTextStyles: { + base: { + backgroundColor: "#000", + border: "1px #f00 solid", + }, + }, + } as RevealElementInput); + }); + } + + if (revealButton) { + revealButton.addEventListener("click", () => { + const revealResponse: Promise = + revealContainer.reveal(); + revealResponse + .then((res: RevealResponse) => { + console.log(res); + }) + .catch((err: SkyflowError) => { + console.log(err); + }); + }); + } + }) + .catch((err: SkyflowError) => { + const errorElement = document.getElementById( + "collectResponse" + ) as HTMLElement; + if (errorElement) { + errorElement.innerHTML = JSON.stringify(err, null, 2); + } + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.log(err); +} diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/package.json b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/package.json new file mode 100644 index 000000000..89ad4064a --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/package.json @@ -0,0 +1,17 @@ +{ + "name": "skyflowelements", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "parcel src/index.html --open", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "dependencies": { + "skyflow-flowvault-js": "^1.0.0" + }, + "devDependencies": { + "parcel": "^2.0.1" + } +} diff --git a/samples/using-typescript/skyflow-elements/src/index.html b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/src/index.html similarity index 100% rename from samples/using-typescript/skyflow-elements/src/index.html rename to packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/src/index.html diff --git a/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/src/index.ts b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/src/index.ts new file mode 100644 index 000000000..8cf323eae --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-typescript/skyflow-elements/src/index.ts @@ -0,0 +1,273 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import Skyflow, { + CollectContainer, + CollectElement, + CollectElementInput, + CollectElementOptions, + CollectResponse, + ErrorTextStyles, + InputStyles, + LabelStyles, + RevealContainer, + RevealElement, + RevealElementInput, + RevealOptions, + RevealResponse, + SkyflowConfig, + SkyflowError, +} from 'skyflow-flowvault-js'; + +try { + const revealView = document.getElementById('revealView') as HTMLElement; + if (revealView) { + revealView.style.visibility = 'hidden'; + } + const config: SkyflowConfig = { + vaultID: '', + vaultURL: '', + getBearerToken: () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4 && Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } + }; + const url = ''; + Http.open('GET', url); + Http.send(); + }); + }, + options: { + logLevel: Skyflow.LogLevel.ERROR, + env: Skyflow.Env.PROD, + } + } + const skyflowClient: Skyflow = Skyflow.init(config); + + // Create collect Container. + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + + // Custom styles for collect elements. + const collectStylesOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + requiredAsterisk:{ + color: 'red' + } + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as ErrorTextStyles, + }; + + // Create collect elements. + const cardNumberInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + } + const cardNumberOptions: CollectElementOptions = { + required: true + } + const cardNumberElement: CollectElement = collectContainer.create(cardNumberInput, cardNumberOptions); + + const cvvInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'cvv', + ...collectStylesOptions, + label: 'Cvv', + placeholder: 'cvv', + type: Skyflow.ElementType.CVV, + } + const cvvElement: CollectElement = collectContainer.create(cvvInput); + + const expiryDateInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + } + const expiryDateElement: CollectElement = collectContainer.create(expiryDateInput); + + const cardholderNameInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + } + const cardHolderNameElement: CollectElement = collectContainer.create(cardholderNameInput); + + // Mount the elements. + cardNumberElement.mount('#collectCardNumber'); + cvvElement.mount('#collectCvv'); + expiryDateElement.mount('#collectExpiryDate'); + cardHolderNameElement.mount('#collectCardholderName'); + + // Collect all elements data. + const collectButton = document.getElementById('collectPCIData') as HTMLButtonElement; + if (collectButton) { + collectButton.addEventListener('click', () => { + const collectResponse: Promise = collectContainer.collect(); + collectResponse + .then((response: CollectResponse) => { + console.log(response); + response = response; + const responseElement = document.getElementById('collectResponse') as HTMLElement; + if (responseElement) { + responseElement.innerHTML = JSON.stringify(response, null, 2); + } + + if (revealView) { + revealView.style.visibility = 'visible'; + } + + const revealStyleOptions = { + inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + marginTop: '4px', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as InputStyles, + labelStyles: { + base: { + fontSize: '16px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as LabelStyles, + errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + } as ErrorTextStyles, + }; + + // Create Reveal Elements With Tokens. + const fieldsTokenData = response.records![0].tokens!; + const revealContainer = skyflowClient.container( + Skyflow.ContainerType.REVEAL + ) as RevealContainer; + + const revealCardNumberInput: RevealElementInput = { + token: fieldsTokenData.card_number[0].token, + label: 'Card Number', + ...revealStyleOptions, + } + const revealCardNumberElement: RevealElement = revealContainer.create(revealCardNumberInput); + revealCardNumberElement.mount('#revealCardNumber'); + + const revealCardCvvInput: RevealElementInput = { + token: fieldsTokenData.cvv[0].token, + label: 'CVV', + ...revealStyleOptions, + altText: '###', + } + const revealCardCvvElement: RevealElement = revealContainer.create(revealCardCvvInput); + revealCardCvvElement.mount('#revealCvv'); + + const revealCardExpiryInput: RevealElementInput = { + token: fieldsTokenData.expiry_date[0].token, + label: 'Card Expiry Date', + ...revealStyleOptions, + } + const revealCardExpiryElement: RevealElement = revealContainer.create(revealCardExpiryInput); + revealCardExpiryElement.mount('#revealExpiryDate'); + + const revealCardholderNameInput: RevealElementInput = { + token: fieldsTokenData.first_name[0].token, + label: 'Card Holder Name', + ...revealStyleOptions, + } + const revealCardholderNameElement: RevealElement = revealContainer.create(revealCardholderNameInput); + revealCardholderNameElement.mount('#revealCardholderName'); + + const revealButton = document.getElementById('revealPCIData') as HTMLButtonElement; + + if (revealButton) { + revealButton.addEventListener('click', () => { + // Redaction is applied per token group via reveal options. + const revealOptions: RevealOptions = { + tokenGroupRedactions: [ + { + tokenGroupName: 'deterministic', + redaction: 'redacted', + }, + ], + }; + const revealResponse: Promise = revealContainer.reveal(revealOptions) + revealResponse.then((res: RevealResponse) => { + console.log(res); + }).catch((err: SkyflowError) => { + console.log(err); + }); + }); + } + }) + .catch((err: SkyflowError) => { + console.log(err); + }); + }); + } +} catch (err: unknown) { + console.log(err); +} \ No newline at end of file diff --git a/packages/skyflow-flowvault-js/src/api-utils/collect.ts b/packages/skyflow-flowvault-js/src/api-utils/collect.ts new file mode 100644 index 000000000..35564f771 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/api-utils/collect.ts @@ -0,0 +1,337 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB (/v2) collect data layer: request builders, response/error parsers, and +// the insert/update transport variants. The generic element-collection step +// (`constructElementsInsertReq`) is reused from @core — not redefined — and +// re-exported here so consumers import it from the flowvault collect surface. +import omit from 'lodash/omit'; +import { safeMerge } from '@core/utils/safe-merge'; +import { + IInsertRecordInput, IInsertRecord, CVVMap, +} from '@core/types'; +import Client from '@core/client'; +import { checkDuplicateColumns } from '@core/api-utils/collect'; +import { normalizeFlowDBError } from '../libs/skyflow-flowdb-error'; +import { generateMockCVV } from '../utils/helpers'; +import { IFlowDBUpsertOptions } from '../utils/common'; +import { + FlowDBInsertRecordData, + FlowDBInsertRequestBody, + FlowDBInsertResponseBody, + CollectResponse, + CollectRecord, + CollectError, + FlowDBUpdateRecordData, + FlowDBUpdateRequestBody, + FlowDBUpsert, +} from '../internal/internal-types'; + +// Generic element-collection assembly. NOTE: unlike @core's privacyDB variant, +// flowDB's AdditionalFields records use the flowDB shape `{ tableName, data, +// skyflowId }` (vs privacyDB `{ table, fields }`), so the additionalFields merge +// is variant-specific and this cannot reuse @core's constructElementsInsertReq. +// The element-collection loop that follows is identical in both variants +// (accepted loose-coupling duplication). +export const constructElementsInsertReq = (req, update, options) => { + let tables = Object.keys(req); + let ids = Object.keys(update); + const additionalFields = options?.additionalFields; + if (additionalFields) { + // merge additionalFields in req + additionalFields.records.forEach((record) => { + const { tableName, data, skyflowId } = record; + if (skyflowId) { + if (ids.includes(skyflowId)) { + checkDuplicateColumns(data, update[skyflowId], tableName); + const temp = { ...data }; + safeMerge(temp, update[skyflowId]); + update[skyflowId] = temp; + } else { + update[skyflowId] = { + ...data, + table: tableName, + }; + } + } else if (tables.includes(tableName)) { + checkDuplicateColumns(data, req[tableName], tableName); + const temp = { ...data }; + safeMerge(temp, req[tableName]); + req[tableName] = temp; + } else { + req[tableName] = { ...data }; + } + }); + } + const records: IInsertRecord[] = []; + const updateRecords: IInsertRecord[] = []; + + tables = Object.keys(req); + tables.forEach((table) => { + records.push({ + table, + fields: req[table], + }); + }); + ids = Object.keys(update); + ids.forEach((id) => { + updateRecords.push({ + table: update[id].table, + fields: update[id], + skyflowID: id, + }); + }); + return [{ records }, { updateRecords }]; +}; + +const getFlowDBUpsertForTable = ( + tableName: string, + options: Array | undefined, +): FlowDBUpsert | undefined => { + if (!options) return undefined; + const match = options.find((upsertOption) => upsertOption.tableName === tableName); + if (!match) return undefined; + return { + uniqueColumns: match.uniqueColumns, + ...(match.updateType ? { updateType: match.updateType } : {}), + }; +}; + +export const constructFlowDBInsertRequest = ( + records: IInsertRecordInput, + options: Record = { tokens: true }, + vaultID: string | undefined, +): FlowDBInsertRequestBody => { + const insertRecords: FlowDBInsertRecordData[] = records.records.map((record) => { + const upsert = getFlowDBUpsertForTable(record.table, options?.upsert); + return { + tableName: record.table, + data: record.fields, + ...(upsert ? { upsert } : {}), + }; + }); + + return { + vaultID, + records: insertRecords, + }; +}; + +export const constructFlowDBInsertResponse = ( + responseBody: FlowDBInsertResponseBody, +): CollectResponse => { + const records: Array = []; + + responseBody.records.forEach((res) => { + if (res.error) { + records.push({ + error: res.error, + tableName: res.tableName, + httpCode: res.httpCode, + }); + return; + } + const hasHashedData = res.hashedData && Object.keys(res.hashedData).length > 0; + records.push({ + tableName: res.tableName, + ...(res.skyflowID ? { skyflowId: res.skyflowID } : {}), + tokens: res.tokens ?? {}, + ...(hasHashedData ? { hashedData: res.hashedData } : {}), + httpCode: res.httpCode, + }); + }); + + return { records }; +}; + +// CVVMap now lives in @core/types (variant-neutral). Re-exported here so existing +// consumers of the flowvault collect surface keep resolving it from this module. +export type { CVVMap }; + +/** + * Replaces the token of every CVV column in the collect response with a mock 3/4-digit + * placeholder. The mock matches the length of the value the user entered and never equals it. + * + * The FlowDB response keys `tokens` by the top-level column name; each value is a list of token + * entries. A flat column's entries carry no `path`; a nested JSON column exposes each subfield as + * a separate entry carrying a dotted `path` (e.g. `city.street`) relative to that top-level + * column. So the captured CVV column (which may be a dotted path like `address.city.street`) is + * split at the FIRST dot into the top-level key + the remaining path, and the token is targeted: + * - flat column (no nested path): replace the path-less entries (all of them, e.g. one per token + * group), applying the same mock so they stay consistent. + * - nested column: replace only the entry whose `path` EXACTLY equals the remaining path. Exact + * equality (not a prefix) keeps parent and child paths isolated, since e.g. `city`, + * `city.street` and `city.ward` legitimately coexist in the same array. + * hashedData and non-CVV columns are left untouched. + */ +export const replaceCVVTokensInResponse = ( + records: Array, + cvvMap: CVVMap, +): Array => { + if (!records) return records; + records.forEach((record) => { + if (!record || !record.tokens) return; + const tokens = record.tokens as Record; + let columnMap: Record | undefined; + if (record.skyflowId && cvvMap.update[record.skyflowId]) { + columnMap = cvvMap.update[record.skyflowId]; + } else if (record.tableName && cvvMap.insert[record.tableName]) { + columnMap = cvvMap.insert[record.tableName]; + } + if (!columnMap) return; + Object.keys(columnMap).forEach((column) => { + const dotIndex = column.indexOf('.'); + const topKey = dotIndex === -1 ? column : column.slice(0, dotIndex); + const nestedPath = dotIndex === -1 ? undefined : column.slice(dotIndex + 1); + if (!(topKey in tokens)) return; + const enteredValue = columnMap![column]; + // An empty entered CVV has no sensitive value to mask; replace its token with an empty + // string. This also avoids calling generateMockCVV with length 0, which has no mock. + const mock = enteredValue ? generateMockCVV(enteredValue.length) : ''; + const tokenValue = tokens[topKey]; + if (Array.isArray(tokenValue)) { + tokenValue.forEach((entry) => { + if (!entry || typeof entry !== 'object' || !('token' in entry)) return; + if (nestedPath === undefined) { + if (entry.path === undefined) entry.token = mock; + } else if (entry.path === nestedPath) { + entry.token = mock; + } + }); + } else if (nestedPath === undefined) { + if (tokenValue && typeof tokenValue === 'object' && 'token' in tokenValue) { + tokenValue.token = mock; + } else { + tokens[topKey] = mock; + } + } + }); + }); + return records; +}; + +// Merge the flowDB insert + update responses that fire together in one collect. +// Each response is either a success / per-record-partial-failure body +// ({ records }, where an individual record may carry an inline `error`) or a +// full endpoint failure ({ error }, no records). Outcomes: +// - at least one endpoint returned records → resolve { records }, folding every +// fully-failed endpoint into a synthesized inline error record so its failure +// is not lost (mirrors flowDB collect's existing per-record partial-failure +// contract, rather than discarding the successful sibling). httpCode is +// included on the synthesized record only when the error envelope carried a +// numeric code. +// - every endpoint fully failed (nothing landed) → surface the first { error } +// so the caller rejects, unchanged from a single full failure. +// Returns a plain union so the caller (skyflow-frame-controller) decides +// resolve vs reject; kept pure so it is unit-testable without the frame harness. +export const mergeFlowDBCollectResponses = ( + responses: any[], + cvvMap: CVVMap, +): CollectResponse | CollectError => { + const records: CollectRecord[] = responses.reduce( + (acc, response) => acc.concat(response?.records || []), + [] as CollectRecord[], + ); + const failures = responses.filter((response) => response?.error !== undefined); + if (failures.length !== 0 && records.length === 0) { + return failures[0] as CollectError; + } + replaceCVVTokensInResponse(records, cvvMap); + failures.forEach((failure) => { + const httpCode = Number(failure.error?.httpCode); + records.push({ + error: failure.error?.message ?? '', + ...(Number.isFinite(httpCode) ? { httpCode } : {}), + }); + }); + return { records }; +}; + +export const constructFlowDBInsertError = (error: any): CollectError => { + const rawError = error?.data?.error; + if (rawError) { + return { error: normalizeFlowDBError(rawError) }; + } + return { + error: { + httpCode: error?.error?.code, + message: error?.error?.description, + }, + }; +}; + +export const constructFlowDBUpdateRequest = ( + updateRecords: { updateRecords: IInsertRecord[] }, + options: Record = { tokens: true }, + vaultID: string | undefined, +): FlowDBUpdateRequestBody => { + // `updateType` is now sourced per-record from the matching table's upsert entry + // (there is no top-level options.updateType). If a record's table has no upsert + // entry, `updateType` is omitted — same result as the previous no-top-level case. + const upsertOptions: IFlowDBUpsertOptions[] = Array.isArray(options?.upsert) + ? options.upsert : []; + const records: FlowDBUpdateRecordData[] = updateRecords.updateRecords.map((record) => { + const upsertMatch = upsertOptions.find((upsert) => upsert.tableName === record.table); + return { + skyflowID: record.skyflowID as string, + tableName: record.table, + data: omit(record.fields, ['table', 'skyflowID']), + ...(upsertMatch?.updateType ? { updateType: upsertMatch.updateType } : {}), + }; + }); + + return { + vaultID, + records, + }; +}; + +// When the flowDB API rejects with a non-2xx status it can still return a body +// carrying a `records` key (partial failure). In that case resolve the request +// through the success constructor so the per-record results/errors flow to the +// client, and only fall back to the top-level error envelope on a full failure. +const parseFlowDBError = (error: any) => { + if (Array.isArray(error?.data?.records)) { + return constructFlowDBInsertResponse(error.data); + } + return constructFlowDBInsertError(error); +}; + +// Insert and update share the same transport: POST a pre-built flowDB request body, +// parse the success/partial-failure/full-failure response the same way — they differ +// only in the endpoint. So there is one writer parameterized by URL, not a per-op +// variant object. +const executeFlowDBWrite = ( + url: string, + requestBody: FlowDBInsertRequestBody | FlowDBUpdateRequestBody, + client: Client, + authToken: string, +) => new Promise((resolve) => { + client?.request({ + body: JSON.stringify(requestBody), + requestMethod: 'POST', + url, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + resolve(constructFlowDBInsertResponse(response)); + }) + ?.catch((error: any) => { + resolve(parseFlowDBError(error)); + }); +}); + +export const insertDataInCollectFlowDB = async ( + requestBody: FlowDBInsertRequestBody, + client: Client, + authToken: string, +) => executeFlowDBWrite(`${client.config.vaultURL}/v2/records/insert`, requestBody, client, authToken); + +export const updateDataInCollectFlowDB = async ( + requestBody: FlowDBUpdateRequestBody, + client: Client, + authToken: string, +) => executeFlowDBWrite(`${client.config.vaultURL}/v2/records/update`, requestBody, client, authToken); diff --git a/packages/skyflow-flowvault-js/src/api-utils/reveal.ts b/packages/skyflow-flowvault-js/src/api-utils/reveal.ts new file mode 100644 index 000000000..7dfed000e --- /dev/null +++ b/packages/skyflow-flowvault-js/src/api-utils/reveal.ts @@ -0,0 +1,323 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB (/v2) reveal/detokenize data layer: request builder, response/error +// parsers, the detokenize transport variant, the token-fetch entrypoints +// (element + composable), and the client-facing response formatters. The +// variant-neutral iframe formatter (`formatRecordsForIframe`) lives in @core and +// is consumed by the reveal frame, not here. +import { getAccessToken } from '@core/utils/bus-events'; +import { formatForPureJsFailure } from '@core/api-utils/reveal'; +import { + IRevealRecord, + IRevealRecordComposable, + IRevealResponseType, + MessageType, + LogLevel, +} from '@core/types'; +import Client from '@core/client'; +import { printLog } from '../utils/logs-helper'; +import { normalizeFlowDBError } from '../libs/skyflow-flowdb-error'; +import { + FlowDBDetokenizeRequestBody, + FlowDBDetokenizeResponseBody, + FlowDBDetokenizeResponse, + FlowDBDetokenizeRequestError, + RevealResponse, + RevealRecordMetadata, + RevealError, +} from '../internal/internal-types'; + +export const constructFlowDBDetokenizeRequest = ( + tokenIdRecords: IRevealRecord[] | IRevealRecordComposable[], + vaultID: string | undefined, + options?: Record, +): FlowDBDetokenizeRequestBody => { + const tokens = tokenIdRecords.map((record) => record.token as string); + + const tokenGroupRedactions = options?.tokenGroupRedactions; + + return { + vaultID, + tokens, + ...(Array.isArray(tokenGroupRedactions) && tokenGroupRedactions.length > 0 + ? { tokenGroupRedactions } + : {}), + }; +}; + +export const constructFlowDBDetokenizeResponse = ( + responseBody: FlowDBDetokenizeResponseBody, +): FlowDBDetokenizeResponse => { + const records: FlowDBDetokenizeResponse['records'] = []; + const errors: FlowDBDetokenizeResponse['errors'] = []; + (responseBody?.response || []).forEach((res) => { + if (res.error) { + errors.push({ + token: res.token, + error: { code: res.httpCode, description: res.error }, + }); + return; + } + const hasMetadata = res.metadata && Object.keys(res.metadata).length > 0; + records.push({ + token: res.token, + value: res.value, + ...(res.tokenGroupName ? { tokenGroupName: res.tokenGroupName } : {}), + ...(hasMetadata ? { metadata: res.metadata } : {}), + httpCode: res.httpCode, + }); + }); + return { records, errors }; +}; + +export const constructFlowDBDetokenizeError = ( + error: any, +): FlowDBDetokenizeRequestError => ({ + errors: [ + { + token: '', + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + // Pass the raw API error body through for the element/composable reveal contract, + // normalized to the SDK camelCase convention. flowDB returns a flat error envelope + // ({ grpcCode, httpCode, message, httpStatus, details }); accept that or a nested + // { error } shape, and fall back to the SkyflowError envelope only when there is no + // raw body (e.g. non-JSON responses). + error: error?.data + ? normalizeFlowDBError(error?.data?.error ?? error.data) + : { httpCode: error?.error?.code, message: error?.error?.description }, +}); + +interface IDetokenizeVariant { + buildRequest( + client: Client, + tokenIdRecords: IRevealRecord[] | IRevealRecordComposable[], + options: Record | undefined, + authToken: string, + ): Promise | undefined; + parseSuccess(response: any): FlowDBDetokenizeResponse; + parseError(error: any): FlowDBDetokenizeResponse | FlowDBDetokenizeRequestError; +} + +// When the flowDB detokenize API rejects with a non-2xx status it can still return +// a body carrying a `response` array (partial failure). In that case route it through +// the success constructor so per-token results/errors flow to the client, and only +// fall back to the top-level error envelope on a full failure. +const parseFlowDBDetokenizeError = ( + error: any, +): FlowDBDetokenizeResponse | FlowDBDetokenizeRequestError => { + if (Array.isArray(error?.data?.response)) { + return constructFlowDBDetokenizeResponse(error.data); + } + return constructFlowDBDetokenizeError(error); +}; + +const flowDBDetokenizeVariant: IDetokenizeVariant = { + buildRequest: (client, tokenIdRecords, options, authToken) => client?.request({ + body: JSON.stringify( + constructFlowDBDetokenizeRequest(tokenIdRecords, client.config.vaultID, options), + ), + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/tokens/detokenize`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }), + parseSuccess: (response) => constructFlowDBDetokenizeResponse(response), + parseError: (error) => parseFlowDBDetokenizeError(error), +}; + +const executeDetokenize = ( + variant: IDetokenizeVariant, + tokenIdRecords: IRevealRecord[] | IRevealRecordComposable[], + client: Client, + options: Record | undefined, + authToken: string, +): Promise => new Promise((resolve) => { + try { + variant.buildRequest(client, tokenIdRecords, options, authToken) + ?.then((response: any) => { + resolve(variant.parseSuccess(response)); + }) + ?.catch((error: any) => { + resolve(variant.parseError(error)); + }); + } catch (error) { + resolve(variant.parseError(error)); + } +}); + +export const fetchRecordsByTokenIdFlowDB = ( + tokenIdRecords: IRevealRecord[], + client: Client, + options?: Record, +): Promise => new Promise((rootResolve, rootReject) => { + const clientId = client.toJSON()?.metaData?.uuid || ''; + getAccessToken(clientId).then((authToken) => { + executeDetokenize( + flowDBDetokenizeVariant, tokenIdRecords, client, options, authToken as string, + ).then((result) => { + // Element contract: a full API failure surfaces the raw body as a top-level { error }. + if ((result as FlowDBDetokenizeRequestError).error + && !(result as FlowDBDetokenizeResponse).records) { + rootReject({ error: (result as FlowDBDetokenizeRequestError).error }); + return; + } + const successRecords = (result as FlowDBDetokenizeResponse).records || []; + const failedRecords = (result.errors || []).map((errRecord) => { + const errorData = formatForPureJsFailure( + { error: { code: errRecord.error?.code, description: errRecord.error?.description } }, + errRecord.token, + false, + ); + printLog(errorData.error?.description || '', MessageType.ERROR, LogLevel.ERROR); + return errorData; + }); + if (failedRecords.length === 0) { + // flowDB records are richer than @core IRevealResponseType's + // Record[]; the runtime shape is the flowDB contract. + rootResolve({ records: successRecords as any }); + } else if (successRecords.length === 0) { + rootReject({ errors: failedRecords }); + } else { + rootReject({ records: successRecords, errors: failedRecords }); + } + }); + }).catch((err) => { + rootReject(err); + }); +}); + +export const fetchRecordsByTokenIdComposableFlowDB = ( + tokenIdRecords: IRevealRecordComposable[], + client: Client, + authToken: string, + options?: Record, +): Promise => new Promise((rootResolve, rootReject) => { + const frameIdByToken: Record = {}; + tokenIdRecords?.forEach((record) => { + frameIdByToken[record?.token ?? ''] = record?.iframeName ?? ''; + }); + + executeDetokenize(flowDBDetokenizeVariant, tokenIdRecords, client, options, authToken) + .then((result) => { + // Full API failure: surface the raw body as a top-level { error }. + if ((result as FlowDBDetokenizeRequestError).error + && !(result as FlowDBDetokenizeResponse).records) { + rootReject({ error: (result as FlowDBDetokenizeRequestError).error }); + return; + } + const recordsResponse: Record[] = []; + const errorResponse: Record[] = []; + + ((result as FlowDBDetokenizeResponse).records || []).forEach((record) => { + recordsResponse.push({ + 0: { + token: record.token, + value: record.value, + ...(record.tokenGroupName ? { tokenGroupName: record.tokenGroupName } : {}), + ...(record.metadata ? { metadata: record.metadata } : {}), + httpCode: record.httpCode, + }, + frameId: frameIdByToken[record.token] ?? '', + }); + }); + + (result.errors || []).forEach((errRecord) => { + const errorData = formatForPureJsFailure( + { error: { code: errRecord.error?.code, description: errRecord.error?.description } }, + errRecord.token, + false, + ); + printLog(errorData?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + errorResponse.push({ + ...errorData, + frameId: frameIdByToken[errRecord.token] ?? '', + }); + }); + + if (errorResponse.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse.length === 0) { + rootReject({ errors: errorResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorResponse }); + } + }); +}); + +const normalizeFlowDBMetadata = (metadata: Record): RevealRecordMetadata => { + const result: Record = { ...metadata }; + if (Object.prototype.hasOwnProperty.call(result, 'table')) { + result.tableName = result.table; + delete result.table; + } + if (Object.prototype.hasOwnProperty.call(result, 'skyflowID')) { + result.skyflowId = result.skyflowID; + delete result.skyflowID; + } + return result; +}; + +export const formatRecordsForClientFlowDB = ( + response: any, +): RevealResponse | RevealError => { + // Full API failure: pass the raw error body straight through. + if (response?.error) { + return { error: response.error }; + } + const records: RevealResponse['records'] = []; + (response?.records || []).forEach((record: any) => { + records.push({ + token: record.token, + ...(record.tokenGroupName ? { tokenGroupName: record.tokenGroupName } : {}), + ...(record.metadata && Object.keys(record.metadata).length > 0 + ? { metadata: normalizeFlowDBMetadata(record.metadata) } : {}), + httpCode: record.httpCode, + }); + }); + (response?.errors || []).forEach((errorRecord: any) => { + records.push({ + error: errorRecord.error?.description ?? errorRecord.error, + token: errorRecord.token, + httpCode: errorRecord.error?.code, + }); + }); + return { records }; +}; + +export const formatRecordsForClientComposableFlowDB = (response) => { + // Full API failure: pass the raw error body straight through. + if (response?.error) { + return { error: response.error }; + } + + const records: any[] = []; + + (response?.records || []).forEach((record) => { + const data = record?.[0] ?? {}; + records.push({ + token: data.token ?? '', + ...(data.tokenGroupName ? { tokenGroupName: data.tokenGroupName } : {}), + ...(data.metadata && Object.keys(data.metadata).length > 0 + ? { metadata: normalizeFlowDBMetadata(data.metadata) } : {}), + httpCode: data.httpCode, + }); + }); + + (response?.errors || []).forEach((errorRecord) => { + records.push({ + error: errorRecord?.error?.description ?? errorRecord?.error, + token: errorRecord?.token ?? '', + httpCode: errorRecord?.error?.code, + }); + }); + + return { records }; +}; diff --git a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts new file mode 100644 index 000000000..d201ebd83 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts @@ -0,0 +1,82 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB collect container: the shared @core CollectContainer base bound to +// flowDB's collect option/response types, plus the injected divergence — +// create() (remaps the client-facing tableName→table and uses flowDB's +// collect-input validator), token handling (flowDB forces tokens on and does +// not validate) and SkyflowFlowDBError error mapping. flowDB has no file upload, +// so there is no uploadFiles. The element interfaces are re-exported from @core +// under the same public names (imported as './collect-container'). +import CoreCollectContainer, { + ICollectElementBase, +} from '@core/external/collect/collect-container'; +import { VariantCollectAdapter } from '@core/types'; +import { + validateCollectElementInput, + validateCollectElementOptions, + validateFlowDBAdditionalFieldsInCollect, + validateFlowDBUpsertOptions, +} from '../../utils/validators'; +import { + CollectElementInput, CollectElementOptions, CollectElementUpdateOptions, ICollectOptions, +} from '../../utils/common'; +import { CollectResponse } from '../../internal/internal-types'; +import SkyflowFlowDBError from '../../libs/skyflow-flowdb-error'; +import collectVariant from './collect-variant'; + +export type { + ElementGroupItem, ElementGroup, +} from '@core/external/collect/collect-container'; + +// flowDB collect-element descriptor: shared base + flowDB `tableName` key. See 2.4. +export interface ICollectElement extends ICollectElementBase { + tableName?: string; +} + +class CollectContainer extends CoreCollectContainer< +ICollectOptions, CollectResponse, CollectElementUpdateOptions, +CollectElementInput, CollectElementOptions +> { + // flowDB collect key strategy (client-facing `skyflowId`/`tableName`); single + // source is this package's VariantAdapter, injected into each element. + protected collectVariant: VariantCollectAdapter = collectVariant; + + protected validateCreateInput(input: CollectElementInput): void { + validateCollectElementInput(input, this.context.logLevel); + } + + // Map the client-facing `tableName` key onto the internal `table` name that the + // rest of the collect pipeline consumes. `returnMockValue` reaches the element + // via formatOptions (which spreads all options), so it is validated here — the + // one create() seam with flowDB options — rather than folded into the fields. + // eslint-disable-next-line class-methods-use-this + protected buildCreateElementFields( + input: CollectElementInput, + options: CollectElementOptions, + ): Record { + validateCollectElementOptions(options); + return { table: input.tableName }; + } + + // flowDB validates additionalFields/upsert against the flowDB key shapes + // (tableName/uniqueColumns/data), then forces tokens on (flowDB has no + // client-facing `tokens`). It does NOT delegate to the @core base validator, + // whose upsert/additionalFields checks are privacyDB-shaped (table/column/fields). + // eslint-disable-next-line class-methods-use-this + protected validateCollectOptions(options: ICollectOptions): ICollectOptions { + if (options?.additionalFields) { + validateFlowDBAdditionalFieldsInCollect(options.additionalFields); + } + if (options?.upsert) { + validateFlowDBUpsertOptions(options.upsert); + } + return { ...options, tokens: true } as ICollectOptions; + } + + // eslint-disable-next-line class-methods-use-this + protected wrapCollectError(err: any): any { + return err ? new SkyflowFlowDBError(err) : err; + } +} +export default CollectContainer; diff --git a/packages/skyflow-flowvault-js/src/external/collect/collect-variant.ts b/packages/skyflow-flowvault-js/src/external/collect/collect-variant.ts new file mode 100644 index 000000000..0dc5da5fb --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/collect/collect-variant.ts @@ -0,0 +1,24 @@ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// flowDB collect-side key normalization, bound onto the shared @core collect +// containers' `collectVariant` protected field. flowDB accepts the client-facing +// `skyflowId`/`tableName` and remaps them onto the internal `skyflowID`/`table` +// the SET_VALUE handler consumes; it carries the id as `skyflowId`. +import { VariantCollectAdapter } from '@core/types'; + +const collectVariant: VariantCollectAdapter = { + normalizeUpdateOptions: (options) => { + if (Object.prototype.hasOwnProperty.call(options, 'skyflowId')) { + options.skyflowID = options.skyflowId; + delete options.skyflowId; + } + if (Object.prototype.hasOwnProperty.call(options, 'tableName')) { + options.table = options.tableName; + delete options.tableName; + } + }, + skyflowIdKey: 'skyflowId', +}; + +export default collectVariant; diff --git a/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts new file mode 100644 index 000000000..c9c39b889 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts @@ -0,0 +1,68 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB composable collect container: the shared @core CoreComposableCollectContainer +// (controller-frame bootstrap, mount/grid layout, createMultipleElement, collect(), +// on(), the bus COMPOSABLE_CONTAINER handshake and updateListeners) bound to +// flowDB's collect response type, plus the injected divergence — create() (remaps +// the client-facing tableName→table and uses flowDB's collect-input validator), +// token handling (flowDB forces tokens on and does not validate) and +// SkyflowFlowDBError error mapping. flowDB has no file upload, so there is no +// uploadFiles and the base's empty registerElementListeners default is inherited. +import CoreComposableCollectContainer from '@core/external/collect/composable-collect-container'; +import { VariantCollectAdapter } from '@core/types'; +import { CollectElementInput, CollectElementOptions, ICollectOptions } from '../../utils/common'; +import { CollectResponse } from '../../internal/internal-types'; +import { + validateCollectElementInput, + validateCollectElementOptions, + validateFlowDBAdditionalFieldsInCollect, + validateFlowDBUpsertOptions, +} from '../../utils/validators'; +import SkyflowFlowDBError from '../../libs/skyflow-flowdb-error'; +import collectVariant from './collect-variant'; + +class ComposableContainer extends CoreComposableCollectContainer< +ICollectOptions, CollectResponse, CollectElementInput, CollectElementOptions +> { + // flowDB collect key strategy (`skyflowId`/`tableName`); injected into each element. + protected collectVariant: VariantCollectAdapter = collectVariant; + + protected validateCreateInput(input: CollectElementInput): void { + validateCollectElementInput(input, this.context.logLevel); + } + + // Map the client-facing `tableName` key onto the internal `table` name that the + // rest of the collect pipeline consumes. `returnMockValue` reaches the element + // via formatOptions (which spreads all options), so it is validated here — the + // one create() seam with flowDB options — rather than folded into the fields. + // eslint-disable-next-line class-methods-use-this + protected buildCreateElementFields( + input: CollectElementInput, + options: CollectElementOptions, + ): Record { + validateCollectElementOptions(options); + return { table: input.tableName }; + } + + // flowDB validates additionalFields/upsert against the flowDB key shapes + // (tableName/uniqueColumns/data), then forces tokens on (flowDB has no + // client-facing `tokens`). It does NOT delegate to the @core base validator, + // whose upsert/additionalFields checks are privacyDB-shaped (table/column/fields). + // eslint-disable-next-line class-methods-use-this + protected validateCollectOptions(options: ICollectOptions): ICollectOptions { + if (options?.additionalFields) { + validateFlowDBAdditionalFieldsInCollect(options.additionalFields); + } + if (options?.upsert) { + validateFlowDBUpsertOptions(options.upsert); + } + return { ...options, tokens: true } as ICollectOptions; + } + + // eslint-disable-next-line class-methods-use-this + protected wrapCollectError(err: any): any { + return err ? new SkyflowFlowDBError(err) : err; + } +} +export default ComposableContainer; diff --git a/packages/skyflow-flowvault-js/src/external/collect/compose-collect-element.ts b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-element.ts new file mode 100644 index 000000000..3bf9dafc5 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-element.ts @@ -0,0 +1,7 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// The composable collect element is variant-agnostic and lives in @core; +// re-exported here so the flowDB package path (index-node export, tests) stays +// stable and mirrors the privacyDB package layout. +export { default } from '@core/external/collect/composable-collect-element'; diff --git a/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-container.ts b/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-container.ts new file mode 100644 index 000000000..17629611b --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-container.ts @@ -0,0 +1,87 @@ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// flowDB composable reveal container: the shared @core reveal base plus the +// flowDB-only surface — create() (its token-only reveal-input and element), the +// token-only record + options validators, forwarding reveal options into the +// frame payload, and the SkyflowFlowDBError full-failure mapping. +import logs from '@core/utils/logs'; +import CoreComposableRevealContainer from '@core/external/reveal/composable-reveal-container'; +import { MessageType } from '@core/types'; +import { printLog, parameterizedString } from '../../utils/logs-helper'; +import SkyflowFlowDBError from '../../libs/skyflow-flowdb-error'; +import ComposableRevealElement from './composable-reveal-element'; +import ComposableRevealInternalElement from './composable-reveal-internal'; +import { IRevealElementOptions } from './reveal-container'; +import { IFlowDBRevealElementInput, IRevealOptions } from '../../utils/common'; +import { RevealResponse } from '../../internal/internal-types'; +import { + validateRevealElementRecords, + validateRevealOptions, +} from '../../utils/validators'; + +class ComposableRevealContainer + extends CoreComposableRevealContainer { + create = (input: IFlowDBRevealElementInput, options?: IRevealElementOptions) => { + const { elementName, controllerIframeName } = this.buildComposableRevealElement(input, options); + return new ComposableRevealElement(elementName, + this.eventEmitter, + controllerIframeName); + }; + + protected instantiateInternalElement( + elementId: string, + tempElements: any, + ): ComposableRevealInternalElement { + return new ComposableRevealInternalElement( + elementId, + tempElements, + this.metaData, + { + containerId: this.containerId, + isMounted: this.containerMounted, + type: this.type, + eventEmitter: this.eventEmitter, + }, + this.context, + ); + } + + // eslint-disable-next-line class-methods-use-this + protected validateRecords(records: any[]): void { + validateRevealElementRecords(records); + } + + // eslint-disable-next-line class-methods-use-this + protected validateOptions(options?: IRevealOptions): void { + validateRevealOptions(options); + } + + // eslint-disable-next-line class-methods-use-this + protected revealExtraData(options?: IRevealOptions): Record { + return { options }; + } + + protected handleRevealResponse( + revealData: any, + resolve: (value: any) => void, + reject: (reason?: any) => void, + ): void { + if (revealData?.error) { + printLog( + parameterizedString(logs?.errorLogs?.FAILED_REVEAL), + MessageType.ERROR, + this.context?.logLevel, + ); + reject(new SkyflowFlowDBError(revealData.error)); + } else { + printLog( + parameterizedString(logs?.infoLogs?.REVEAL_SUBMIT_SUCCESS, this.getClassName()), + MessageType.LOG, + this.context?.logLevel, + ); + resolve(revealData); + } + } +} +export default ComposableRevealContainer; diff --git a/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-element.ts b/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-element.ts new file mode 100644 index 000000000..09e7a07f4 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-element.ts @@ -0,0 +1,7 @@ +import CoreComposableRevealElement from '@core/external/reveal/composable-reveal-element'; +import { IRevealElementInput } from './reveal-container'; + +// flowDB composable reveal element: the shared @core base bound to flowDB's +// token-only reveal-input shape. No renderFile — file-render is privacyDB-only. +export default class ComposableRevealElement + extends CoreComposableRevealElement {} diff --git a/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-internal.ts b/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-internal.ts new file mode 100644 index 000000000..39d755538 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/reveal/composable-reveal-internal.ts @@ -0,0 +1,16 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// flowDB composable reveal-internal element: the shared @core base bound to +// flowDB's token-only reveal-input shape. No renderFile — file-render is +// privacyDB-only (the base's registerRenderFileRequestListener hook stays empty). +import CoreComposableRevealInternalElement from '@core/external/reveal/composable-reveal-internal'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; + +export interface RevealComposableGroup{ + record: IRevealElementInput + options: IRevealElementOptions +} + +export default class ComposableRevealInternalElement + extends CoreComposableRevealInternalElement {} diff --git a/packages/skyflow-flowvault-js/src/external/reveal/reveal-container.ts b/packages/skyflow-flowvault-js/src/external/reveal/reveal-container.ts new file mode 100644 index 000000000..5457a73ed --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/reveal/reveal-container.ts @@ -0,0 +1,65 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB reveal container: the shared @core RevealContainer base bound to +// flowDB's token-only types, plus the injected divergence — the RevealElement +// factory, the token-only reveal-record validator, reveal-options validation, +// and SkyflowFlowDBError error mapping. The flowDB reveal input/option TYPE +// definitions live in `../../utils/common`; they are re-exported here under +// the shared public names so the reveal element/composable files that import +// them from `./reveal-container` keep resolving. +import CoreRevealContainer from '@core/external/reveal/reveal-container'; +import { + ICoreMetadata, RevealContainerProps, Context, +} from '@core/types'; +import SkyflowFlowDBError from '../../libs/skyflow-flowdb-error'; +import type { RevealResponse } from '../../internal/internal-types'; +import type { + IFlowDBRevealElementInput, + IRevealElementOptions, + IRevealOptions, + TokenGroupRedaction, +} from '../../utils/common'; +import { validateRevealElementRecords, validateRevealOptions } from '../../utils/validators'; +import RevealElement from './reveal-element'; + +// The flowDB reveal input is the token-only shape; `IRevealElementInput` aliases +// it (no privacyDB skyflowID/table/column/file-render keys). +export type IRevealElementInput = IFlowDBRevealElementInput; +export type { + IFlowDBRevealElementInput, + IRevealElementOptions, + IRevealOptions, + TokenGroupRedaction, +}; + +class RevealContainer + extends CoreRevealContainer { + // eslint-disable-next-line class-methods-use-this + protected createRevealElement( + record: IRevealElementInput, + options: IRevealElementOptions | undefined, + metaData: ICoreMetadata, + container: RevealContainerProps, + elementId: string, + context: Context, + ): RevealElement { + return new RevealElement(record, options, metaData, container, elementId, context); + } + + // eslint-disable-next-line class-methods-use-this + protected validateRecords(records: IRevealElementInput[]): void { + validateRevealElementRecords(records); + } + + // eslint-disable-next-line class-methods-use-this + protected validateOptions(options?: IRevealOptions): void { + validateRevealOptions(options); + } + + // eslint-disable-next-line class-methods-use-this + protected wrapRevealError(err: any): any { + return new SkyflowFlowDBError(err); + } +} +export default RevealContainer; diff --git a/packages/skyflow-flowvault-js/src/external/reveal/reveal-element.ts b/packages/skyflow-flowvault-js/src/external/reveal/reveal-element.ts new file mode 100644 index 000000000..b7740c1e3 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/reveal/reveal-element.ts @@ -0,0 +1,9 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// flowDB reveal element: the shared @core reveal-element base bound to flowDB's +// token-only reveal-input shape. No renderFile — file-render is privacyDB-only. +import CoreRevealElement from '@core/external/reveal/reveal-element'; +import { IRevealElementInput } from './reveal-container'; + +export default class RevealElement extends CoreRevealElement {} diff --git a/packages/skyflow-flowvault-js/src/external/skyflow-container.ts b/packages/skyflow-flowvault-js/src/external/skyflow-container.ts new file mode 100644 index 000000000..f042d066e --- /dev/null +++ b/packages/skyflow-flowvault-js/src/external/skyflow-container.ts @@ -0,0 +1,8 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowvault is elements-only: its controller-frame bootstrap is exactly the +// shared @core SkyflowContainer base (no pure-JS insert/detokenize/get/delete/ +// update surface — see package-split §9 decisions). Re-exported here so this +// package's own import path (skyflow.ts) is unchanged. +export { default } from '@core/external/skyflow-container'; diff --git a/packages/skyflow-flowvault-js/src/index-internal.ts b/packages/skyflow-flowvault-js/src/index-internal.ts new file mode 100644 index 000000000..a3d4457d4 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/index-internal.ts @@ -0,0 +1,73 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import 'core-js/stable'; +import { + COMPOSABLE_REVEAL, + FRAME_ELEMENT, + FRAME_REVEAL, + SKYFLOW_FRAME_CONTROLLER, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import RevealComposableFrameElementInit from './internal/composable-frame-element-init'; +import RevealFrame from './internal/reveal/reveal-frame'; +import SkyflowFrameController from './internal/skyflow-frame/skyflow-frame-controller'; +import { MessageType, LogLevel } from './utils/common'; +import { + printLog, + parameterizedString, + getElementName, +} from './utils/logs-helper'; +import { getAtobValue, getValueFromName } from './utils/helpers'; +import FrameElementInit from './internal/frame-element-init'; + +// Iframe entry for skyflow-flowvault-js. Structurally parallel to skyflow-js's +// index-internal over the same @core skeleton, wired to flowvault's own frame +// controllers (collect tokenize / reveal detokenize). Elements-only: no file +// upload/render frame paths. + +// The composable reveal frame's variant seam (reveal transport + the DOM-heavy +// RevealFrame) is bound in the package subclass RevealComposableFrameElementInit +// above — no runtime variant registry. This entry runs only in the iframe +// bundle, so RevealFrame stays out of the main-thread browser/node bundles. + +(function init(root: any) { + try { + const frameName = root.name; + const frameType = getValueFromName(frameName, 0); + const frameId = getValueFromName(frameName, 1); + if (frameType === SKYFLOW_FRAME_CONTROLLER) { + SkyflowFrameController.init(frameId); + } else if (frameType === COMPOSABLE_REVEAL) { + root.Skyflow = RevealComposableFrameElementInit; + RevealComposableFrameElementInit.startFrameElement(); + } else if (frameType === FRAME_ELEMENT) { + const logLevel = getValueFromName(frameName, 4) || LogLevel.ERROR; + printLog( + parameterizedString( + logs.infoLogs.COLLECT_ELEMET_START, + 'index-internal', + getElementName(frameName), + ), + MessageType.LOG, + LogLevel[logLevel], + ); + root.Skyflow = FrameElementInit; + FrameElementInit.startFrameElement(); + } else if (frameType === FRAME_REVEAL) { + const logLevel = getValueFromName(frameName, 3) || LogLevel.ERROR; + printLog( + parameterizedString( + logs.infoLogs.REVEAL_ELEMENT_START, + 'index-internal', + getAtobValue(frameId), + ), + MessageType.LOG, + LogLevel[logLevel], + ); + RevealFrame.init(); + } + } catch (e) { + throw new Error(parameterizedString(logs.errorLogs.INVALID_IFRAME)); + } +}(window)); diff --git a/packages/skyflow-flowvault-js/src/index-node.ts b/packages/skyflow-flowvault-js/src/index-node.ts new file mode 100644 index 000000000..18a3ed973 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/index-node.ts @@ -0,0 +1,99 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Public Node barrel for skyflow-flowvault-js. Surfaces the flowDB public +// contract under the agreed names: flowDB-specific input/response/option types, +// the element container/element classes, the shared variant-neutral enums/types +// (re-exported from @core via ./utils/common and @core/constants), and the +// public error class SkyflowError (= SkyflowFlowDBError). flowvault is +// elements-only: no pure-JS request/response types, no 3DS, no file upload. +import CoreCollectElement from '@core/external/collect/collect-element'; +import CoreComposableElement from './external/collect/compose-collect-element'; +import type { CollectElementUpdateOptions } from './utils/common'; +import Skyflow from './skyflow'; + +// --- flowDB collect / reveal input + option types --- +export { + CollectElementInput, + CollectElementOptions, + CollectElementUpdateOptions, + ICollectOptions as CollectOptions, + IFlowDBUpsertOptions as UpsertOptions, + AdditionalFields, + AdditionalFieldsRecord, + IFlowDBRevealElementInput as RevealElementInput, + IRevealElementOptions as RevealElementOptions, + IRevealOptions as RevealOptions, + TokenGroupRedaction, + UpdateType, + ContainerOptions, + // Shared variant-neutral enums / types (re-exported from @core by ./utils/common) + // NOTE: no RequestMethod — flowDB is elements-only (no connection/gateway APIs). + RedactionType, + ElementType, + ValidationRuleType, + IValidationRule as ValidationRule, + EventName, + LogLevel, + Env, + ElementState, + ErrorType, + ErrorMessages, + InputStyles, + LabelStyles, + ErrorTextStyles, + CardMetadata, +} from './utils/common'; + +// --- flowDB public response types --- +export { + CollectResponse, + CollectRecord, + CollectRecordToken, + CollectRecordHashedData, + RevealResponse, + RevealRecord, + RevealRecordMetadata, +} from './internal/internal-types'; + +// flowDB's public ElementType (base only) is exported from ./utils/common above; +// @core only holds the internal BaseElementType / FileElementType. +export { + CardType, +} from '@core/constants'; + +export { + ContainerType, + ISkyflow as SkyflowConfig, +} from './skyflow'; + +// --- element container / element classes --- +// The @core element classes are generic over their update-options type, +// defaulting to the identity-neutral base (no `tableName`/`skyflowId`). Bind them +// to flowDB's CollectElementUpdateOptions so the published `update()` accepts +// `{ tableName, skyflowId }`. The runtime value stays the real @core class (so +// `instanceof` is preserved); only the exported TYPE is parameterized. The +// value/type pair shares one name across namespaces (legal in TS; no-redeclare +// can't tell). +export const CollectElement = CoreCollectElement; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type CollectElement = CoreCollectElement; +export const ComposableElement = CoreComposableElement; +// flowDB is elements-only (no MULTI_FILE_INPUT), so the shared @core class's +// uploadMultipleFiles() can only ever throw MULTI_FILE_NOT_SUPPORTED here. Hide +// it from the published TYPE so it isn't dead file-API surface; the runtime value +// stays the real @core class (instanceof preserved). See audit finding F5. +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ComposableElement = Omit, 'uploadMultipleFiles'>; + +export { default as CollectContainer } from './external/collect/collect-container'; +export { default as ComposableContainer } from './external/collect/compose-collect-container'; +export { default as RevealContainer } from './external/reveal/reveal-container'; +export { default as RevealElement } from './external/reveal/reveal-element'; +export { default as ComposableRevealContainer } from './external/reveal/composable-reveal-container'; +export { default as ComposableRevealElement } from './external/reveal/composable-reveal-element'; + +// Public error surface: the flowDB error IS the package's `SkyflowError`. +export { default as SkyflowError } from './libs/skyflow-flowdb-error'; + +export default Skyflow; diff --git a/packages/skyflow-flowvault-js/src/index.ts b/packages/skyflow-flowvault-js/src/index.ts new file mode 100644 index 000000000..4ed2084b5 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/index.ts @@ -0,0 +1,14 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import 'core-js/stable'; +import Skyflow from './skyflow'; + +// Browser/UMD entry for skyflow-flowvault-js. Exposes the flowvault SDK on the +// `Skyflow` global — same name as skyflow-js. The two packages are separate +// bundles with different APIs, so a page must not load both via script tag +// (last one loaded wins the `Skyflow` global); the rare dual-use case is served +// by aliasing on the module import instead. +(function init(root: any) { + root.Skyflow = root.Skyflow || Skyflow; +}(window)); diff --git a/packages/skyflow-flowvault-js/src/internal/composable-frame-element-init.ts b/packages/skyflow-flowvault-js/src/internal/composable-frame-element-init.ts new file mode 100644 index 000000000..8fa04cb68 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/internal/composable-frame-element-init.ts @@ -0,0 +1,49 @@ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// flowDB composable reveal-frame init: the shared @core base bound to flowDB's +// reveal transport (api-utils/reveal `*ComposableFlowDB` mappers) and its +// `RevealFrame`. Instantiated only from src/index-internal (iframe bundle) via +// the static factory, so `RevealFrame` stays out of the main-thread bundles. +import CoreRevealComposableFrameElementInit from '@core/internal/composable-frame-element-init'; +import { Context, IRevealRecordComposable, IRevealResponseType } from '@core/types'; +import { + fetchRecordsByTokenIdComposableFlowDB, + formatRecordsForClientComposableFlowDB, +} from '../api-utils/reveal'; +import RevealFrame from './reveal/reveal-frame'; + +class RevealComposableFrameElementInit extends CoreRevealComposableFrameElementInit { + private static frameEle?: RevealComposableFrameElementInit; + + static startFrameElement = () => { + RevealComposableFrameElementInit.frameEle = new RevealComposableFrameElementInit(); + }; + + // eslint-disable-next-line class-methods-use-this + protected fetchRecordsByTokenIdComposable( + tokenIdRecords: IRevealRecordComposable[], + client: any, + authToken: string, + options?: Record, + ): Promise { + return fetchRecordsByTokenIdComposableFlowDB(tokenIdRecords, client, authToken, options); + } + + // eslint-disable-next-line class-methods-use-this + protected formatRecordsForClientComposable(response: any): Record { + return formatRecordsForClientComposableFlowDB(response); + } + + // eslint-disable-next-line class-methods-use-this + protected createRevealFrame( + record: any, + context: Context, + containerId: string, + rootDiv?: HTMLDivElement, + ) { + return new RevealFrame(record, context, containerId, rootDiv); + } +} + +export default RevealComposableFrameElementInit; diff --git a/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts b/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts new file mode 100644 index 000000000..fb977606c --- /dev/null +++ b/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts @@ -0,0 +1,103 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB composable-collect controller-frame init: the shared @core base +// (validation, request-object assembly, DOM/grid build, message handling) plus the +// injected divergence — dispatchCollectRequest(), which builds the flowDB /v2 +// insert/update requests, dispatches them, aggregates the response and masks CVV +// tokens. flowDB has no file upload, so the base's no-op file-message hooks are +// inherited. +import CoreFrameElementInit from '@core/internal/frame-element-init'; +import Client from '@core/client'; +import { CVVMap, ErrorType } from '@core/types'; +import { + constructElementsInsertReq, + constructFlowDBInsertRequest, + constructFlowDBUpdateRequest, + insertDataInCollectFlowDB, + updateDataInCollectFlowDB, + replaceCVVTokensInResponse, +} from '../api-utils/collect'; + +export default class FrameElementInit extends CoreFrameElementInit { + private static frameEle?: FrameElementInit; + + // flowDB captures CVV values into `cvvMap` for response masking (privacyDB does not). + protected collectsCVV = true; + + static startFrameElement = () => { + FrameElementInit.frameEle = new FrameElementInit(); + }; + + // eslint-disable-next-line class-methods-use-this + protected dispatchCollectRequest( + insertRequestObject: any, + updateRequestObject: any, + cvvMap: CVVMap, + options: any, + clientConfig: any, + errorMessages?: Record, + ): Promise { + let finalInsertRequest; + let finalInsertRecords; + let finalUpdateRecords; + let finalUpdateRequest; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertRequestObject, updateRequestObject, options, + ); + finalInsertRequest = constructFlowDBInsertRequest( + finalInsertRecords, options, clientConfig.vaultID, + ); + finalUpdateRequest = constructFlowDBUpdateRequest( + finalUpdateRecords, options, clientConfig.vaultID, + ); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + const client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + if (errorMessages && client) { + client.setErrorMessages(errorMessages); + } + const sendRequest = () => new Promise((rootResolve, rootReject) => { + const insertPromiseSet: Promise[] = []; + + if (finalInsertRecords.records.length !== 0) { + insertPromiseSet.push( + insertDataInCollectFlowDB( + finalInsertRequest, client, clientConfig.authToken as string, + ), + ); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + insertPromiseSet.push( + updateDataInCollectFlowDB( + finalUpdateRequest, client, clientConfig.authToken as string, + ), + ); + } + if (insertPromiseSet.length !== 0) { + Promise.all(insertPromiseSet).then((responses: any[]) => { + const failure = responses.find((response) => response?.error !== undefined); + if (failure) { + rootReject(failure); + return; + } + const records = responses.reduce( + (acc, response) => acc.concat(response?.records || []), + [] as any[], + ); + replaceCVVTokensInResponse(records, cvvMap); + rootResolve({ records }); + }); + } + }); + + return sendRequest(); + } +} diff --git a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts new file mode 100644 index 000000000..744f63f5d --- /dev/null +++ b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts @@ -0,0 +1,233 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB (/v2) internal request/response wire types. These describe the flowDB +// vault API bodies and the normalized records/errors surfaced to the element +// iframes and the public response contract. Ported from the 2.9.0-beta.1 tag; +// RedactionType is reused from @core (not redefined), UpdateType from the +// flowvault common surface. +import { + RedactionType, ContainerType, ClientMetadata, ElementInfo, ICollectResponseBase, + IRevealResponseBase, +} from '@core/types'; +import { BaseElementType } from '@core/constants'; +import { UpdateType, ICollectOptions } from '../../utils/common'; + +// COLLECT tokenize input consumed by the skyflow-frame-controller. Mirrors the +// privacyDB shape but over flowDB's ICollectOptions. +export interface TokenizeDataInput extends ICollectOptions { + type: string; + elementIds: Array; + containerId: string; +} + +// Variant-neutral internal types re-exported from @core so copied element files' +// `../../internal/internal-types` imports resolve here. The flowDB wire types +// below are defined locally. +export type { + ElementInfo, + ContainerProps, + RevealContainerProps, + InternalState, + BatchInsertRequestBody, + FormattedCollectElementOptions, + ClientMetadata, +} from '@core/types'; + +// Element metadata surfaced to the collect element/iframe classes. Mirrors the +// privacyDB `Metadata` but with loosely-typed clientJSON / skyflowContainer +// (flowvault does not import the privacyDB container classes). +export interface SkyflowElementProps { + id: string; + type: BaseElementType; + element: HTMLElement; + container: any; +} + +export interface Metadata extends ClientMetadata { + clientJSON: any; + containerType: ContainerType; + skyflowContainer: any; + getSkyflowBearerToken: () => Promise; +} + +// --- Collect (insert / update) request bodies --- + +export interface FlowDBUpsert { + updateType?: UpdateType; + uniqueColumns: string[]; +} + +export interface FlowDBInsertRecordData { + data: Record; + tokens?: Record; + tableName?: string; + upsert?: FlowDBUpsert; +} + +export interface FlowDBInsertRequestBody { + vaultID: string | undefined; + tableName?: string; + records: FlowDBInsertRecordData[]; + upsert?: FlowDBUpsert; +} + +export interface FlowDBUpdateRecordData { + skyflowID: string; + data: Record; + tokens?: Record; + tableName?: string; + updateType?: 'UPDATE' | 'REPLACE'; +} + +export interface FlowDBUpdateRequestBody { + vaultID: string | undefined; + tableName?: string; + records: FlowDBUpdateRecordData[]; + updateType?: 'UPDATE' | 'REPLACE'; +} + +// --- Collect response (raw vault + normalized public contract) --- + +export interface FlowDBRecordResponse { + skyflowID: string | null; + tokens?: Record; + data?: Record; + hashedData?: Record; + error?: string | null; + // Insert/update responses (RecordResponseObject) always carry httpCode — the + // flowdb.proto marks it `required` — so it is non-optional here and needs no cast. + httpCode: number; + tableName: string; +} + +export interface FlowDBInsertResponseBody { + records: FlowDBRecordResponse[]; +} + +export interface FlowDBError { + code?: number | string; + description?: string; +} + +// Public flowDB collect record/response (surfaced to the SDK consumer). +// `tokens` and `hashedData` are keyed by the dynamic column name (e.g. +// "card_number"); each value is a list of per-token / per-hash entries. +export interface CollectRecordToken { + token: string; + tokenGroupName?: string; + // Present only for nested JSON columns (dotted/bracketed sub-path, e.g. + // "street", "phone_numbers[1].number[0]"); flat columns omit it. + path?: string; +} + +export interface CollectRecordHashedData { + data: string; + hashName: string; +} + +export interface CollectRecord { + tableName?: string; + skyflowId?: string; + tokens?: Record; + hashedData?: Record; + // Real insert/update records (RecordResponseObject) always carry httpCode (the + // flowdb.proto marks it `required`), but a mixed collect also folds a + // fully-failed sibling endpoint into a synthesized inline error record whose + // httpCode is only present when the error envelope carried a numeric code — + // hence optional on the public contract. + httpCode?: number; + error?: string; +} + +export interface CollectResponse extends ICollectResponseBase { + records: Array; +} + +// Full-failure body (no records) — normalized into SkyflowFlowDBError. +export interface FlowDBFullError { + grpcCode?: number; + httpCode?: number | string; + message?: string; + httpStatus?: string; + details?: any[]; +} + +export interface CollectError { + error: FlowDBFullError; +} + +// --- Detokenize (reveal) request/response bodies --- + +export interface FlowDBTokenGroupRedaction { + tokenGroupName: string; + redaction: RedactionType | string; +} + +export interface FlowDBDetokenizeRequestBody { + vaultID: string | undefined; + tokens: string[]; + tokenGroupRedactions?: FlowDBTokenGroupRedaction[]; +} + +export interface FlowDBDetokenizeResponseObject { + token: string; + value?: any; + tokenGroupName?: string | null; + error?: string | null; + httpCode?: number; + metadata?: Record; +} + +export interface FlowDBDetokenizeResponseBody { + response: FlowDBDetokenizeResponseObject[]; +} + +export interface FlowDBDetokenizeResponseRecord { + token: string; + value?: any; + tokenGroupName?: string | null; + metadata?: Record; + httpCode?: number; +} + +export interface FlowDBDetokenizeResponseRecordError { + token: string; + error: FlowDBError; +} + +export interface FlowDBDetokenizeResponse { + records: FlowDBDetokenizeResponseRecord[]; + errors: FlowDBDetokenizeResponseRecordError[]; +} + +export interface FlowDBDetokenizeRequestError { + errors: FlowDBDetokenizeResponseRecordError[]; + // Raw full-failure body passed through for the element/composable reveal contract. + error?: FlowDBFullError; +} + +// Public flowDB reveal record/response (surfaced to the SDK consumer). +export interface RevealRecordMetadata { + skyflowId?: string; + tableName?: string; +} + +export interface RevealRecord { + token: string; + tokenGroupName?: string; + metadata?: RevealRecordMetadata; + // Detokenize responses (FlowDetokenizeResponseObject) do NOT mark httpCode + // `required` in flowdb.proto, and the error path sources it from `error?.code`, + // so it can be absent — optional to keep the public contract honest. + httpCode?: number; + error?: string; +} + +export interface RevealResponse extends IRevealResponseBase { + records: Array; +} + +export interface RevealError { + error: FlowDBFullError; +} diff --git a/packages/skyflow-flowvault-js/src/internal/reveal/reveal-frame.ts b/packages/skyflow-flowvault-js/src/internal/reveal/reveal-frame.ts new file mode 100644 index 000000000..1e2e00d58 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/internal/reveal/reveal-frame.ts @@ -0,0 +1,9 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB reveal is token-only, so it uses the shared @core reveal-frame base +// verbatim — the base already omits the privacyDB file-render path (which lives +// in skyflow-js's subclass via the base's protected hooks). Re-exported here so +// this package's own import path (index-internal, jest.setup, the +// createRevealFrame factory) is unchanged. +export { default } from '@core/internal/reveal/reveal-frame'; diff --git a/packages/skyflow-flowvault-js/src/internal/skyflow-frame/skyflow-frame-controller.ts b/packages/skyflow-flowvault-js/src/internal/skyflow-frame/skyflow-frame-controller.ts new file mode 100644 index 000000000..c3d188776 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/internal/skyflow-frame/skyflow-frame-controller.ts @@ -0,0 +1,149 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB skyflow-frame controller. Elements-only: Collect + Reveal (+ composable), +// no file operations and no pure-JS data methods. It extends the shared @core base +// (CoreSkyflowFrameController) and supplies only the flowDB API-call divergence — +// the flowDB collect send path (sendCollectRequest: /v2 insert + update in one +// Promise.all, with CVV token re-mapping) and the flowDB reveal fetch/format. +// Telemetry identity + the error-envelope shape are injected via +// getSdkNameAndVersion/wrapCallbackError. Because it never overrides +// registerDataAccessListeners, the base leaves it without the PUREJS_REQUEST / +// PUREJS_FRAME_READY channel; the handleExtra* hooks stay no-ops (no file paths). +import { getAccessToken } from '@core/utils/bus-events'; +import { injectCoralogixTrackingScript } from '@core/helpers'; +import { SdkInfo } from '@core/client'; +import { ICollectedElementsData } from '@core/internal/skyflow-frame/collect-elements'; +import CoreSkyflowFrameController from '@core/internal/skyflow-frame/skyflow-frame-controller-base'; +import { + constructElementsInsertReq, + constructFlowDBInsertRequest, + constructFlowDBUpdateRequest, + insertDataInCollectFlowDB, + updateDataInCollectFlowDB, + mergeFlowDBCollectResponses, +} from '../../api-utils/collect'; +import { + fetchRecordsByTokenIdFlowDB, + formatRecordsForClientFlowDB, +} from '../../api-utils/reveal'; +import { IRevealRecord } from '../../utils/common'; +import { getSDKNameAndVersion } from '../../utils/helpers'; +import { + FlowDBInsertRequestBody, FlowDBUpdateRequestBody, + TokenizeDataInput, CollectResponse, RevealResponse, RevealError, +} from '../internal-types'; + +// flowDB reveal result: a normal response or a full-failure error body (the +// base's revealData rejects with the latter). Aliased to keep the class header +// within line length. +type FlowDBRevealResult = RevealResponse | RevealError; + +class SkyflowFrameController + extends CoreSkyflowFrameController { + // flowDB captures CVV values into cvvMap for post-response token re-mapping. + protected collectsCVV = true; + + // flowDB resolves a partial reveal failure ({ records, errors }) as success. + protected revealResolvesPartialFailure = true; + + static init(clientId: string = ''): SkyflowFrameController { + injectCoralogixTrackingScript(); + return new SkyflowFrameController(clientId); + } + + // flowDB telemetry identity (deliberate loose-coupling duplication: each + // package owns its telemetry helper). + // eslint-disable-next-line class-methods-use-this + protected getSdkNameAndVersion(metaData?: string): SdkInfo { + return getSDKNameAndVersion(metaData); + } + + // flowDB error envelope: forward an already-enveloped body as-is (avoids + // double-nesting the flowDB error), otherwise wrap. + // eslint-disable-next-line class-methods-use-this + protected wrapCallbackError(error: any): any { + return error?.error !== undefined ? error : { error }; + } + + protected fetchRevealRecords( + revealRecords: IRevealRecord[], + options?: Record, + ): Promise { + return fetchRecordsByTokenIdFlowDB(revealRecords, this.client, options); + } + + // eslint-disable-next-line class-methods-use-this + protected formatRevealForClient(result: any): FlowDBRevealResult { + return formatRecordsForClientFlowDB(result); + } + + // flowDB collect send path: build the flowDB /v2 insert + update requests, fire + // them together, merge records, and re-map CVV tokens in the response. + protected sendCollectRequest( + built: ICollectedElementsData, + options: TokenizeDataInput, + ): Promise { + const { insertResponseObject, updateResponseObject, cvvMap } = built; + let finalInsertRequest: FlowDBInsertRequestBody; + let finalUpdateRequest: FlowDBUpdateRequestBody; + let finalInsertRecords; + let finalUpdateRecords; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertResponseObject, updateResponseObject, options, + ); + finalInsertRequest = constructFlowDBInsertRequest( + finalInsertRecords, options, this.client.config.vaultID, + ); + finalUpdateRequest = constructFlowDBUpdateRequest( + finalUpdateRecords, options, this.client.config.vaultID, + ); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + const client = this.client; + const sendRequest = (): Promise => new Promise((rootResolve, rootReject) => { + const clientId = client.toJSON()?.metaData?.uuid || ''; + getAccessToken(clientId).then((authToken) => { + const requests: Promise[] = []; + if (finalInsertRecords.records.length !== 0) { + requests.push(insertDataInCollectFlowDB( + finalInsertRequest, + client, + authToken as string, + )); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + requests.push(updateDataInCollectFlowDB( + finalUpdateRequest, + client, + authToken as string, + )); + } + if (requests.length === 0) { + rootResolve({ records: [] }); + return; + } + Promise.all(requests).then((responses: any[]) => { + // A mixed outcome (one endpoint fully fails, the other returns records) + // resolves with the surviving records + an inline error record; only a + // total failure (nothing landed) rejects. See mergeFlowDBCollectResponses. + const merged = mergeFlowDBCollectResponses(responses, cvvMap); + if ('records' in merged) { + rootResolve(merged); + } else { + rootReject(merged); + } + }); + }).catch((err) => { + rootReject(err); + }); + }); + + return sendRequest(); + } +} +export default SkyflowFrameController; diff --git a/packages/skyflow-flowvault-js/src/libs/skyflow-flowdb-error.ts b/packages/skyflow-flowvault-js/src/libs/skyflow-flowdb-error.ts new file mode 100644 index 000000000..06e122010 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/libs/skyflow-flowdb-error.ts @@ -0,0 +1,70 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import SkyflowError, { ISkyflowError } from '@core/errors'; + +export interface IFlowDBError { + grpcCode?: number; + httpCode?: number | string; + message?: string; + httpStatus?: string; + details?: any[]; +} + +// The flowDB API returns error bodies with snake_case keys +// (grpc_code, http_code, http_status). Normalize them to the SDK camelCase +// convention, accepting either casing and emitting only the keys that are +// present so pass-through and fallback shapes stay minimal. Also accepts the +// internal SkyflowError shape (`code`/`description`) and a bare message string +// so every rejection that reaches the wrapper stays informative. +export const normalizeFlowDBError = (raw: any = {}): IFlowDBError => { + if (typeof raw === 'string') return { message: raw }; + const grpcCode = raw?.grpcCode ?? raw?.grpc_code; + const httpCode = raw?.httpCode ?? raw?.http_code ?? raw?.code; + const httpStatus = raw?.httpStatus ?? raw?.http_status; + const message = raw?.message ?? raw?.description; + const { details } = raw ?? {}; + return { + ...(grpcCode !== undefined ? { grpcCode } : {}), + ...(httpCode !== undefined ? { httpCode } : {}), + ...(message !== undefined ? { message } : {}), + ...(httpStatus !== undefined ? { httpStatus } : {}), + ...(details !== undefined ? { details } : {}), + }; +}; + +// Thrown on a full flowDB API failure (an error body without a `records` object). +// Exposes the flowDB error contract in camelCase alongside a standard Error +// `message`. Unlike the 2.9.0-beta.1 version (which extended `Error` directly), +// this extends the variant-neutral @core `SkyflowError` base, so +// `instanceof SkyflowError` holds and flowvault's public `SkyflowError` export +// is this class — while the flowDB-specific fields (grpcCode/httpCode/ +// httpStatus/details) and the camelCase `error` object are added on top. +export default class SkyflowFlowDBError extends SkyflowError { + grpcCode?: number; + + httpCode?: number | string; + + httpStatus?: string; + + details?: any[]; + + // Superset of the neutral base's `ISkyflowError` (so this override is + // assignable to `SkyflowError.error`) carrying the flowDB camelCase fields. + error: IFlowDBError & ISkyflowError; + + constructor(errorBody: any = {}) { + const normalized = normalizeFlowDBError(errorBody); + const code = normalized.httpCode ?? normalized.grpcCode ?? ''; + const description = normalized.message ?? ''; + // Feed the neutral base a structured ISkyflowError so `.message` and + // `instanceof SkyflowError` behave like the shared base. + super({ code, description }, [], true); + this.name = 'SkyflowError'; + this.grpcCode = normalized.grpcCode; + this.httpCode = normalized.httpCode; + this.httpStatus = normalized.httpStatus; + this.details = normalized.details; + this.error = { ...normalized, code, description }; + } +} diff --git a/packages/skyflow-flowvault-js/src/skyflow.ts b/packages/skyflow-flowvault-js/src/skyflow.ts new file mode 100644 index 000000000..95f0e6957 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/skyflow.ts @@ -0,0 +1,109 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowvault (flowDB) public Skyflow shell. The constructor, `static init()`, the +// bearer-token plumbing, the `container()` overloads/switch and the shared static +// enum getters all live in `@core/external/base-skyflow`; this subclass supplies +// only what is genuinely flowDB-specific: +// - the five container factory hooks (which concrete class to `new`) +// - `static get Error()` (SkyflowFlowDBError — the flowDB public error surface) +// and `static get UpdateType()` +// +// flowvault is elements-only: it exposes ONLY the inherited element `container()` +// factory (COLLECT / REVEAL / COMPOSABLE collect / COMPOSE_REVEAL) — no pure-JS +// insert/detokenize/get/delete/update, no 3DS (per package-split §9 decisions). +// COMPOSABLE collect has no file upload (flowDB has none). Its controller-frame +// container is the shared `@core` base as-is, re-exported by +// ./external/skyflow-container. +import Client from '@core/client'; +import BaseSkyflow from '@core/external/base-skyflow'; +import { + ContainerOptions, + ContainerType, + Context, + ICoreMetadata, + ISkyflow, + SkyflowConfigOptions, +} from '@core/types'; +import RevealContainer from './external/reveal/reveal-container'; +import CollectContainer from './external/collect/collect-container'; +import ComposableContainer from './external/collect/compose-collect-container'; +import ComposableRevealContainer from './external/reveal/composable-reveal-container'; +import SkyflowContainer from './external/skyflow-container'; +import SkyflowFlowDBError from './libs/skyflow-flowdb-error'; +import { UpdateType, ElementType } from './utils/common'; + +// Relocated to @core/types (variant-neutral); re-exported here under the same +// names so `./skyflow` importers and the public surface are unchanged. +export { ContainerType }; +export type { ISkyflow, SkyflowConfigOptions }; + +class Skyflow extends BaseSkyflow< +SkyflowContainer, +CollectContainer, +RevealContainer, +ComposableContainer, +ComposableRevealContainer +> { + // ---- Injected divergence: which concrete class to `new` -------------------- + // Prototype methods, not arrow-function fields — `instantiateSkyflowContainer` + // is called from the base constructor, before subclass fields initialize. + + // eslint-disable-next-line class-methods-use-this + protected instantiateSkyflowContainer(client: Client, context: Context): SkyflowContainer { + return new SkyflowContainer(client, context); + } + + // eslint-disable-next-line class-methods-use-this + protected createCollectContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): CollectContainer { + return new CollectContainer(metaData, context, options); + } + + // eslint-disable-next-line class-methods-use-this + protected createRevealContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): RevealContainer { + return new RevealContainer(metaData, context, options); + } + + // eslint-disable-next-line class-methods-use-this + protected createComposableContainer( + metaData: ICoreMetadata, + context: Context, + options: ContainerOptions, + ): ComposableContainer { + return new ComposableContainer(metaData, context, options); + } + + // eslint-disable-next-line class-methods-use-this + protected createComposeRevealContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): ComposableRevealContainer { + return new ComposableRevealContainer(metaData, context, options); + } + + // ---- Package-specific statics (the rest are inherited from BaseSkyflow) ---- + + static get UpdateType() { + return UpdateType; + } + + static get Error() { + return SkyflowFlowDBError; + } + + // flowDB's ElementType (base only — no file elements). Overrides the removed + // @core base getter so the exposed enum matches flowDB's supported surface. + static get ElementType() { + return ElementType; + } +} +export default Skyflow; diff --git a/packages/skyflow-flowvault-js/src/utils/common/index.ts b/packages/skyflow-flowvault-js/src/utils/common/index.ts new file mode 100644 index 000000000..9aaf66b30 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/utils/common/index.ts @@ -0,0 +1,171 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowvault (flowDB) shared type surface. +// +// Variant-neutral types (enums, element/style/config/option interfaces) are +// REUSED from `@core/types` — re-exported here, never redefined — so flowvault +// and skyflow-js stay structurally aligned on the shared pieces. Only the +// flowDB-specific public input types (upsert / reveal input / reveal options) +// and the `UpdateType` enum are defined locally; the flowDB request/response +// bodies live in ../../internal/internal-types. + +// Internal @core base element enum — projected into flowDB's public ElementType +// below. Not re-exported (only the derived ElementType is public). +import { BaseElementType } from '@core/constants'; + +export { + ErrorType, + RedactionType, + EventName, + LogLevel, + Env, + MessageType, + ValidationRuleType, + ContainerType, +} from '@core/types'; + +// flowDB public element-type surface: the shared @core base ONLY. flowDB has no +// file support, so — unlike privacyDB — it does NOT fold in FileElementType. This +// is flowvault's "ElementType extends BaseElementType {}" (enums can't be extended +// in TS, so the base enum is re-projected here). Runtime value + type share one +// name (legal in TS). BaseElementType stays internal to @core, never public. +export const ElementType = { ...BaseElementType }; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ElementType = BaseElementType; +export type { + IRevealElementOptions, + ErrorMessages, + IValidationRule, + Style, + ContainerOptions, + ErrorTextStyles, + LabelStyles, + InputStyles, + FormattedCollectElementOptions, + CardMetadata, + MetaData, + EventConfig, + SkyflowConfigOptions, + ISkyflow, + Context, + ElementInfo, + ContainerProps, + RevealContainerProps, + InternalState, + ClientMetadata, + BatchInsertRequestBody, + MeticsObjectType, + SharedMeticsObjectType, + IRevealRecord, + IRevealRecordComposable, + IRevealResponseType, + IInsertRecordInput, + IDetokenizeInput, + IGetInput, + IGetOptions, + IGetByIdInput, + IDeleteRecordInput, + IUpdateRequest, + IUpdateOptions, +} from '@core/types'; + +// Type-only imports used by the flowDB definitions below. +// eslint-disable-next-line import/first +import type { + ICollectOptionsBase, + ICollectElementOptionsBase, + ICollectElementUpdateOptionsBase, + IElementStateBase, + CollectElementInput as ICoreCollectElementInput, +} from '@core/types'; + +// flowDB collect element options: shared base only — flowDB has no file API, so +// no file options; declared for symmetry + future flowDB-only options. See 2.3. +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CollectElementOptions extends ICollectElementOptionsBase { + returnMockValue?: boolean; +} + +// flowDB element state: shared base + `value` without `Blob` (no file elements). +// See Decision 2.5. +export interface ElementState extends IElementStateBase { + value: string | Object | undefined; +} + +// flowDB collect element input. Extends the neutral input with the flowDB +// client-facing keys `tableName` / `skyflowId` (mapped internally to the +// pipeline's `table` / `skyflowID`). +export interface CollectElementInput extends ICoreCollectElementInput { + tableName?: string; + skyflowId?: string; +} + +// flowDB collect-element update options: shared base + flowDB client-facing identity +// keys. Binds `CollectElement`/`CollectContainer`'s `TUpdateOptions` so +// `element.update()` is typed to flowDB naming (`tableName`/`skyflowId`). See 2.1. +export interface CollectElementUpdateOptions extends ICollectElementUpdateOptionsBase { + tableName?: string; + skyflowId?: string; +} + +// --- flowDB-specific update semantics --- +export enum UpdateType { + UPDATE = 'UPDATE', + REPLACE = 'REPLACE', +} + +// --- flowDB public input types --- + +// Upsert config for a flowDB collect insert (per table). PDB's upsert is +// { table, column }; flowDB's is { tableName, uniqueColumns, updateType } — +// intentionally different (see package-split decisions). +export interface IFlowDBUpsertOptions { + tableName: string; + uniqueColumns: string[]; + updateType?: UpdateType; +} + +// flowDB additionalFields input. Non-PCI data inserted/updated alongside the +// collected elements, in flowDB naming (`tableName`/`data`/`skyflowId`) — +// intentionally distinct from privacyDB's `{ table, fields }` record shape. +// `skyflowId` targets an existing record for update; omit it to insert. +export interface AdditionalFieldsRecord { + tableName: string; + data: Record; + skyflowId?: string; +} + +export interface AdditionalFields { + records: AdditionalFieldsRecord[]; +} + +// flowDB reveal element input — token-based only (no redaction / skyflowID / +// table / column / file-render keys). Redaction is supplied via reveal options. +export interface IFlowDBRevealElementInput { + token?: string; + inputStyles?: object; + label?: string; + labelStyles?: object; + altText?: string; + errorTextStyles?: object; +} + +// Per-token-group redaction supplied through flowDB reveal options. +export interface TokenGroupRedaction { + tokenGroupName: string; + redaction: string; +} + +export interface IRevealOptions { + tokenGroupRedactions?: TokenGroupRedaction[]; +} + +// flowDB collect options. Extends the @core marker. No `tokens` (flowDB forces +// tokens on). `upsert` uses the flowDB upsert shape (IFlowDBUpsertOptions), whose +// per-table `updateType` drives the update variant — there is no top-level +// `updateType`. +export interface ICollectOptions extends ICollectOptionsBase { + additionalFields?: AdditionalFields; + upsert?: Array; +} diff --git a/packages/skyflow-flowvault-js/src/utils/helpers/index.ts b/packages/skyflow-flowvault-js/src/utils/helpers/index.ts new file mode 100644 index 000000000..8acf6b8ca --- /dev/null +++ b/packages/skyflow-flowvault-js/src/utils/helpers/index.ts @@ -0,0 +1,119 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowvault helper subset. Telemetry helpers (SDK identity + the sky-metadata +// header builder) are variant-neutral and mirror skyflow-js's — this is the +// deliberate loose-coupling duplication (each package owns its transport/ +// telemetry). `generateMockCVV` is flowDB-only (mock-CVV masking) and has no +// skyflow-js counterpart. +import * as coreHelpers from '@core/helpers'; +import * as metricsHelper from '@core/utils/metrics-helper'; +import { SdkInfo } from '@core/client'; + +// SDK telemetry identity, injected at build time (webpack DefinePlugin) / tests +// (jest setupFiles) from this package's own package.json. +export const SDK_DETAILS = { name: SDK_NAME, version: SDK_VERSION }; + +// SDK telemetry / device-identity helpers moved to @core/helpers (variant-neutral; +// only input is this bundle's build-injected SDK identity). Re-bound as consts — +// not `export … from` — so jest.spyOn(helpers, …) still hooks them, and existing +// `../helpers` importers/tests resolve them from this single package surface. +export const getSdkVersionName = metricsHelper.getSdkVersionName; + +export function getSDKNameAndVersion(metaData?: string): SdkInfo { + const nameAndVersion: SdkInfo = { + sdkName: SDK_NAME, + sdkVersion: SDK_VERSION, + }; + if (metaData && metaData !== '' && metaData.split('@').length > 1) { + nameAndVersion.sdkName = metaData.split('@')[0]; + nameAndVersion.sdkVersion = metaData.split('@')[1]; + } + return nameAndVersion; +} + +export const getOSDetails = metricsHelper.getOSDetails; + +export const getBrowserInfo = metricsHelper.getBrowserInfo; + +export const getDeviceType = metricsHelper.getDeviceType; + +export const getMetaObject = metricsHelper.getMetaObject; + +export const MOCK_CVV_THREE_DIGIT = '817'; + +export const MOCK_CVV_FOUR_DIGIT = '8173'; + +// Returns the fixed mock CVV that masks a captured value of the given length: +// `817` for a 3-digit CVV, `8173` for a 4-digit CVV, `''` for any other length. +// The mock is a constant, so it may coincide with a real CVV of `817`/`8173` — +// that is acceptable for the GA mock behaviour. flowDB-only. +export const generateMockCVV = (length: number): string => { + switch (length) { + case 3: return MOCK_CVV_THREE_DIGIT; + case 4: return MOCK_CVV_FOUR_DIGIT; + default: return ''; + } +}; + +// --- Variant-neutral element helpers (copied verbatim from skyflow-js helpers; +// the deliberate loose-coupling duplication — each package owns its element DOM +// helpers). Used by the collect element/rendering module graph. --- + +// Re-bound from @core/helpers (definitions moved there so the shared +// @core/internal/iframe-form + collect element layer can reach them). Bound as +// local consts — not `export … from` — so jest.spyOn(helpers, …) still hooks +// them. +export const formatFrameNameToId = coreHelpers.formatFrameNameToId; + +export const removeSpaces = coreHelpers.removeSpaces; + +// Re-bound from @core/helpers (definition moved there so @core/validators and +// core's internal frame layer can reach it). Bound as a local const — not +// `export … from` — so jest.spyOn(helpers, 'appendZeroToOne') still hooks it. +export const appendZeroToOne = coreHelpers.appendZeroToOne; + +export const appendMonthFourDigitYears = coreHelpers.appendMonthFourDigitYears; + +export const appendMonthTwoDigitYears = coreHelpers.appendMonthTwoDigitYears; + +export const getReturnValue = coreHelpers.getReturnValue; + +export const domReady = coreHelpers.domReady; + +export const getMaskedOutput = coreHelpers.getMaskedOutput; + +export const copyToClipboard = coreHelpers.copyToClipboard; + +export const handleCopyIconClick = coreHelpers.handleCopyIconClick; + +export const fileValidation = coreHelpers.fileValidation; + +export const vaildateFileName = coreHelpers.vaildateFileName; + +export const styleToString = coreHelpers.styleToString; + +export const getContainerType = coreHelpers.getContainerType; + +export const addSeperatorToCardNumberMask = coreHelpers.addSeperatorToCardNumberMask; + +// Re-bound from @core/helpers (definition moved there so both SDKs share one +// copy). Bound as a local const — not `export … from` — so jest.spyOn(helpers, +// 'formatVaultURL') still hooks it. +export const formatVaultURL = coreHelpers.formatVaultURL; + +// Re-bound from @core/helpers (definition moved there so the shared +// @core/external/base-skyflow init path can reach it, and so both SDKs share one +// copy). +export const checkAndSetForCustomUrl = coreHelpers.checkAndSetForCustomUrl; + +// Re-bound from @core/helpers (definitions moved there so the shared @core +// reveal-frame base can reach them). Bound as local consts — not `export … from` +// — so jest.spyOn(helpers, …) still hooks them. +export const getValueFromName = coreHelpers.getValueFromName; + +export const getAtobValue = coreHelpers.getAtobValue; + +export const constructMaskTranslation = coreHelpers.constructMaskTranslation; + +export const formatRevealElementOptions = coreHelpers.formatRevealElementOptions; diff --git a/packages/skyflow-flowvault-js/src/utils/logs-helper/index.ts b/packages/skyflow-flowvault-js/src/utils/logs-helper/index.ts new file mode 100644 index 000000000..594d13475 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/utils/logs-helper/index.ts @@ -0,0 +1,10 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// The variant-neutral logging helpers, including `printLog`, live in +// @core/utils/logs-helper (`printLog` reads this bundle's injected +// SDK_NAME/SDK_VERSION). Re-exported here so flowvault code imports them from a +// single package logs-helper surface. +export { + LogLevelOptions, EnvOptions, parameterizedString, getElementName, printLog, +} from '@core/utils/logs-helper'; diff --git a/packages/skyflow-flowvault-js/src/utils/validators/index.ts b/packages/skyflow-flowvault-js/src/utils/validators/index.ts new file mode 100644 index 000000000..14f34b442 --- /dev/null +++ b/packages/skyflow-flowvault-js/src/utils/validators/index.ts @@ -0,0 +1,263 @@ +/* eslint-disable max-len */ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import * as coreValidators from '@core/validators'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import SkyflowError from '@core/errors'; +import { FileElementType } from '@core/constants'; +import { + IFlowDBRevealElementInput as IRevealElementInput, + MessageType, + CollectElementInput, + CollectElementOptions, + LogLevel, + UpdateType, + IFlowDBUpsertOptions, + AdditionalFields, +} from '../common'; +import { printLog } from '../logs-helper'; + +// Re-export the variant-neutral validators now living in @core so the existing +// `../utils/validators` importers keep resolving the full set. Bound as local +// consts (not `export *`) so jest.spyOn(validators, ...) hooks still work — +// star/named re-exports compile to non-configurable getters that can't be spied. +export const validateCreditCardNumber = coreValidators.validateCreditCardNumber; +export const detectCardType = coreValidators.detectCardType; +export const validateExpiryYear = coreValidators.validateExpiryYear; +export const validateExpiryMonth = coreValidators.validateExpiryMonth; +export const isValidExpiryDateFormat = coreValidators.isValidExpiryDateFormat; +export const isValidExpiryYearFormat = coreValidators.isValidExpiryYearFormat; +export const isValidURL = coreValidators.isValidURL; +export const isValidRegExp = coreValidators.isValidRegExp; +export const validateCardNumberLengthCheck = coreValidators.validateCardNumberLengthCheck; +export const validateBooleanOptions = coreValidators.validateBooleanOptions; +export const validateExpiryDate = coreValidators.validateExpiryDate; +export const validateInsertRecords = coreValidators.validateInsertRecords; +export const validateUpdateRecord = coreValidators.validateUpdateRecord; +export const validateAdditionalFieldsInCollect = coreValidators.validateAdditionalFieldsInCollect; +export const validateDetokenizeInput = coreValidators.validateDetokenizeInput; +export const validateGetInput = coreValidators.validateGetInput; +export const validateGetByIdInput = coreValidators.validateGetByIdInput; +export const validateDeleteRecords = coreValidators.validateDeleteRecords; +export const validateInitConfig = coreValidators.validateInitConfig; +export const validateUpsertOptions = coreValidators.validateUpsertOptions; +export const validateComposableContainerOptions = coreValidators.validateComposableContainerOptions; +export const validateInputFormatOptions = coreValidators.validateInputFormatOptions; + +// flowDB reveal-input validators. The flowDB reveal input is token-only +// (IFlowDBRevealElementInput) — no skyflowID/column/table/redaction-per-record or +// file-render keys — so these validate only the token-based surface. +export const validateRevealElementRecords = (records: IRevealElementInput[]) => { + if (records.length === 0) throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORDS_REVEAL, []); + records.forEach((record: any) => { + if (!(record && Object.prototype.hasOwnProperty.call(record, 'token'))) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_TOKEN_KEY_REVEAL, []); + } + if (!record.token) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_TOKEN_ID_REVEAL, []); + } + if (!(typeof record.token === 'string' || record.token instanceof String)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKEN_ID_REVEAL, []); + } + + if (Object.prototype.hasOwnProperty.call(record, 'label') && typeof record.label !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LABEL_REVEAL, []); + } + + if (Object.prototype.hasOwnProperty.call(record, 'altText') && typeof record.altText !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ALT_TEXT_REVEAL, []); + } + }); +}; + +// flowDB-specific reveal-option error codes. These are not present in the shared +// @core SKYFLOW_ERROR_CODE map (they describe the flowDB tokenGroupRedactions +// option, which has no privacyDB counterpart), so they are defined locally here. +const FLOWDB_REVEAL_ERROR_CODE = { + INVALID_TOKEN_GROUP_REDACTIONS_REVEAL: { + code: 400, + description: "Validation error. Invalid 'tokenGroupRedactions' key in reveal options. Specify an array of { tokenGroupName, redaction } objects.", + }, + INVALID_TOKEN_GROUP_REDACTION_ENTRY_REVEAL: { + code: 400, + description: "Validation error. Invalid 'tokenGroupRedactions' entry at index %s1. Specify a non-empty string 'tokenGroupName' and 'redaction'.", + }, +}; + +export const validateRevealOptions = (options?: { tokenGroupRedactions?: any }) => { + if (!options || options.tokenGroupRedactions === undefined) return; + const { tokenGroupRedactions } = options; + if (!Array.isArray(tokenGroupRedactions)) { + throw new SkyflowError(FLOWDB_REVEAL_ERROR_CODE.INVALID_TOKEN_GROUP_REDACTIONS_REVEAL, []); + } + tokenGroupRedactions.forEach((entry: any, index: number) => { + const hasValidName = entry && typeof entry.tokenGroupName === 'string' && entry.tokenGroupName !== ''; + const hasValidRedaction = entry && typeof entry.redaction === 'string' && entry.redaction !== ''; + if (!hasValidName || !hasValidRedaction) { + throw new SkyflowError( + FLOWDB_REVEAL_ERROR_CODE.INVALID_TOKEN_GROUP_REDACTION_ENTRY_REVEAL, [`${index}`], true, + ); + } + }); +}; + +// flowDB collect-element input error codes. These describe flowDB-only create() +// constraints (no file elements; `tableName` is the documented identity key, not +// `table`) that have no privacyDB counterpart, so they are defined locally here +// — mirroring the FLOWDB_REVEAL_ERROR_CODE / FLOWDB_COLLECT_ERROR_CODE blocks. +const FLOWDB_COLLECT_INPUT_ERROR_CODE = { + FILE_ELEMENTS_NOT_SUPPORTED: { + code: 400, + description: "Validation error. File elements ('FILE_INPUT' / 'MULTI_FILE_INPUT') are not supported. Use a supported element type.", + }, + INVALID_TABLE_KEY_IN_COLLECT: { + code: 400, + description: "Validation error. Invalid 'table' key in collect element. Specify 'tableName' instead.", + }, +}; + +// Collect-input validator: emits a package-specific deprecation warning via the +// Tier-1 per-package logs-helper (printLog), so it stays local. +export const validateCollectElementInput = (input: CollectElementInput, logLevel: LogLevel) => { + if (!Object.prototype.hasOwnProperty.call(input, 'type')) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_ELEMENT_TYPE, [], true); + } + if (!input.type) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_TYPE, [], true); + } + if (Object.prototype.hasOwnProperty.call(input, 'altText')) { + printLog(logs.warnLogs.COLLECT_ALT_TEXT_DEPERECATED, MessageType.WARN, logLevel); + } + if (Object.prototype.hasOwnProperty.call(input, 'skyflowId') && !(typeof input.skyflowId === 'string')) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOWID_IN_COLLECT, [], true); + } + // flowDB has no file-element support. flowvault's public ElementType is base-only + // (no file types), so a TS caller can't pass them — but a JS caller still can, so + // reject the raw string values explicitly rather than letting the collect pipeline + // silently drop them. Compared as strings since input.type is typed base-only. + const elementType = input.type as string; + if (elementType === FileElementType.FILE_INPUT || elementType === FileElementType.MULTI_FILE_INPUT) { + throw new SkyflowError(FLOWDB_COLLECT_INPUT_ERROR_CODE.FILE_ELEMENTS_NOT_SUPPORTED, [], true); + } + // flowDB's documented identity key is `tableName` (mapped internally to `table`). + // Reject a client-supplied `table` so the collect and composable-collect paths + // behave identically — otherwise `table` works on one path and breaks on the other. + if (Object.prototype.hasOwnProperty.call(input, 'table')) { + throw new SkyflowError(FLOWDB_COLLECT_INPUT_ERROR_CODE.INVALID_TABLE_KEY_IN_COLLECT, [], true); + } +}; + +// flowDB collect-element options validator. Runs at create() time (via +// buildCreateElementFields) so the check stays flowDB-local — `returnMockValue` +// is a flowDB-only option (privacyDB has no equivalent). Only the type is +// enforced: when present it must be a boolean, otherwise the mock-CVV opt-in +// would be silently coerced to `false`. +export const validateCollectElementOptions = (options?: CollectElementOptions) => { + if (options + && Object.prototype.hasOwnProperty.call(options, 'returnMockValue') + && !coreValidators.validateBooleanOptions(options.returnMockValue)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS, ['returnMockValue'], true); + } +}; + +// flowDB collect-option error codes. flowDB's upsert / additionalFields inputs use +// flowDB naming (`tableName` / `uniqueColumns` / `data`), NOT privacyDB's +// `table` / `column` / `fields`, so the shared @core SKYFLOW_ERROR_CODE messages +// (which name the privacyDB keys) would be misleading here. Defined locally, +// mirroring the FLOWDB_REVEAL_ERROR_CODE block above. +const FLOWDB_COLLECT_ERROR_CODE = { + INVALID_UPSERT_OPTIONS_TYPE: { + code: 400, + description: "Validation error. Invalid 'upsert' options. Specify a non-empty array of { tableName, uniqueColumns } objects.", + }, + INVALID_UPSERT_OPTION_ENTRY: { + code: 400, + description: "Validation error. Invalid 'upsert' entry at index %s1. Specify an object with 'tableName' and 'uniqueColumns'.", + }, + MISSING_TABLE_NAME_IN_UPSERT: { + code: 400, + description: "Validation error. Missing or empty 'tableName' in upsert entry at index %s1. Provide a valid 'tableName'.", + }, + INVALID_UNIQUE_COLUMNS_IN_UPSERT: { + code: 400, + description: "Validation error. Invalid 'uniqueColumns' in upsert entry at index %s1. Provide a non-empty array of column-name strings.", + }, + INVALID_UPDATE_TYPE_IN_UPSERT: { + code: 400, + description: "Validation error. Invalid 'updateType' in upsert entry at index %s1. Use one of 'UPDATE' or 'REPLACE'.", + }, + MISSING_RECORDS_IN_ADDITIONAL_FIELDS: { + code: 400, + description: "Validation error. Missing 'records' key in additionalFields. Specify a non-empty array of { tableName, data } records.", + }, + INVALID_RECORDS_IN_ADDITIONAL_FIELDS: { + code: 400, + description: "Validation error. Invalid 'records' in additionalFields. Specify a non-empty array of { tableName, data } records.", + }, + MISSING_TABLE_NAME_IN_ADDITIONAL_FIELDS: { + code: 400, + description: "Validation error. Missing or empty 'tableName' in additionalFields record at index %s1. Provide a valid 'tableName'.", + }, + INVALID_DATA_IN_ADDITIONAL_FIELDS: { + code: 400, + description: "Validation error. Invalid 'data' in additionalFields record at index %s1. Provide a non-null object of column values.", + }, + INVALID_SKYFLOW_ID_IN_ADDITIONAL_FIELDS: { + code: 400, + description: "Validation error. Invalid 'skyflowId' in additionalFields record at index %s1. Provide a string skyflowId.", + }, +}; + +// flowDB upsert validator: validates the flowDB upsert shape +// ({ tableName, uniqueColumns, updateType? }). Distinct from @core's +// validateUpsertOptions (privacyDB { table, column }). +export const validateFlowDBUpsertOptions = (upsertOptions?: Array) => { + if (!(upsertOptions && Array.isArray(upsertOptions) && upsertOptions.length > 0)) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_UPSERT_OPTIONS_TYPE, [], true); + } + upsertOptions.forEach((option: any, index: number) => { + if (!(option && typeof option === 'object' && !Array.isArray(option))) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_UPSERT_OPTION_ENTRY, [`${index}`], true); + } + if (!(typeof option.tableName === 'string' && option.tableName.length > 0)) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.MISSING_TABLE_NAME_IN_UPSERT, [`${index}`], true); + } + const { uniqueColumns } = option; + const hasValidColumns = Array.isArray(uniqueColumns) + && uniqueColumns.length > 0 + && uniqueColumns.every((column: any) => typeof column === 'string' && column.length > 0); + if (!hasValidColumns) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_UNIQUE_COLUMNS_IN_UPSERT, [`${index}`], true); + } + if (option.updateType !== undefined && !Object.values(UpdateType).includes(option.updateType)) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_UPDATE_TYPE_IN_UPSERT, [`${index}`], true); + } + }); +}; + +// flowDB additionalFields validator: validates the flowDB record shape +// ({ tableName, data, skyflowId? }). Distinct from @core's +// validateAdditionalFieldsInCollect (privacyDB { table, fields }). An empty-string +// skyflowId is accepted (the insert path treats it as "not provided"). +export const validateFlowDBAdditionalFieldsInCollect = (recordObj?: AdditionalFields) => { + if (!(recordObj && Object.prototype.hasOwnProperty.call(recordObj, 'records'))) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.MISSING_RECORDS_IN_ADDITIONAL_FIELDS, [], true); + } + const { records } = recordObj; + if (!(records && Array.isArray(records) && records.length > 0)) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_RECORDS_IN_ADDITIONAL_FIELDS, [], true); + } + records.forEach((record: any, index: number) => { + if (!(record && typeof record.tableName === 'string' && record.tableName.length > 0)) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.MISSING_TABLE_NAME_IN_ADDITIONAL_FIELDS, [`${index}`], true); + } + if (!(record.data && typeof record.data === 'object' && !Array.isArray(record.data))) { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_DATA_IN_ADDITIONAL_FIELDS, [`${index}`], true); + } + if (record.skyflowId !== undefined && typeof record.skyflowId !== 'string') { + throw new SkyflowError(FLOWDB_COLLECT_ERROR_CODE.INVALID_SKYFLOW_ID_IN_ADDITIONAL_FIELDS, [`${index}`], true); + } + }); +}; diff --git a/packages/skyflow-flowvault-js/tests/__mocks__/file-mock.js b/packages/skyflow-flowvault-js/tests/__mocks__/file-mock.js new file mode 100644 index 000000000..92b41df0d --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/__mocks__/file-mock.js @@ -0,0 +1,4 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +module.exports = 'test-file-content'; diff --git a/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js b/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js new file mode 100644 index 000000000..d8b5be7f1 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js @@ -0,0 +1,614 @@ +import Client from '@core/client'; +import { + constructElementsInsertReq, + constructFlowDBInsertRequest, + constructFlowDBInsertResponse, + constructFlowDBInsertError, + constructFlowDBUpdateRequest, + insertDataInCollectFlowDB, + updateDataInCollectFlowDB, + mergeFlowDBCollectResponses, + replaceCVVTokensInResponse, +} from '../../src/api-utils/collect'; + +// Note: the flowvault collect data layer receives the auth token as a parameter +// (from the frame controller), so it does not import @core bus-events — no +// getAccessToken mock is needed here (unlike the skyflow-js privacyDB path). + +const buildClient = () => Client.fromJSON({ config: { vaultID: 'vault123', vaultURL: 'https://vaulturl.com' } }); + +describe('constructFlowDBInsertRequest', () => { + const finalInsertRecords = { + records: [ + { table: 'table1', fields: { card_number: '4111', cvv: '123' } }, + { table: 'table2', fields: { ssn: '999' } }, + ], + }; + + test('defaults options to { tokens: true } when omitted', () => { + const req = constructFlowDBInsertRequest(finalInsertRecords, undefined, 'vault123'); + expect(req.records.map((r) => r.tableName)).toEqual(['table1', 'table2']); + expect(req.records[0].upsert).toBeUndefined(); + }); + + test('maps records to flowDB shape with vaultID at root and tableName per record', () => { + const req = constructFlowDBInsertRequest(finalInsertRecords, { tokens: true }, 'vault123'); + expect(req).toEqual({ + vaultID: 'vault123', + records: [ + { tableName: 'table1', data: { card_number: '4111', cvv: '123' } }, + { tableName: 'table2', data: { ssn: '999' } }, + ], + }); + }); + + test('adds upsert uniqueColumns when upsert option matches table', () => { + const options = { tokens: true, upsert: [{ tableName: 'table1', uniqueColumns: ['card_number'] }] }; + const req = constructFlowDBInsertRequest(finalInsertRecords, options, 'vault123'); + expect(req.records[0].upsert).toEqual({ uniqueColumns: ['card_number'] }); + expect(req.records[1].upsert).toBeUndefined(); + }); + + test('supports multiple uniqueColumns per table', () => { + const options = { + tokens: true, + upsert: [{ tableName: 'table1', uniqueColumns: ['card_number', 'cvv'] }], + }; + const req = constructFlowDBInsertRequest(finalInsertRecords, options, 'vault123'); + expect(req.records[0].upsert).toEqual({ uniqueColumns: ['card_number', 'cvv'] }); + }); + + test('includes updateType in upsert only when provided', () => { + const options = { + tokens: true, + upsert: [ + { tableName: 'table1', uniqueColumns: ['card_number'], updateType: 'REPLACE' }, + { tableName: 'table2', uniqueColumns: ['ssn'] }, + ], + }; + const req = constructFlowDBInsertRequest(finalInsertRecords, options, 'vault123'); + expect(req.records[0].upsert).toEqual({ uniqueColumns: ['card_number'], updateType: 'REPLACE' }); + expect(req.records[1].upsert).toEqual({ uniqueColumns: ['ssn'] }); + expect(req.records[1].upsert.updateType).toBeUndefined(); + }); +}); + +describe('constructFlowDBInsertResponse', () => { + const responseBody = { + records: [ + { + skyflowID: 'id1', + tableName: 'table1', + httpCode: 200, + tokens: { card_number: [{ token: 'tok-1', tokenGroupName: 'nondeterministic' }] }, + }, + ], + }; + + test('builds { tableName, skyflowId, tokens, httpCode }', () => { + const res = constructFlowDBInsertResponse(responseBody); + expect(res).toEqual({ + records: [ + { + tableName: 'table1', + skyflowId: 'id1', + tokens: { + card_number: [{ token: 'tok-1', tokenGroupName: 'nondeterministic' }], + }, + httpCode: 200, + }, + ], + }); + }); + + test('omits skyflowId when null and defaults tokens to empty object', () => { + const body = { + records: [ + { skyflowID: null, tableName: 'table1', httpCode: 200 }, + ], + }; + const res = constructFlowDBInsertResponse(body); + expect(res.records[0].tokens).toEqual({}); + expect(res.records[0]).not.toHaveProperty('skyflowId'); + expect(res.records[0]).not.toHaveProperty('errors'); + }); + + test('inlines per-record error into records without a skyflowId field', () => { + const body = { + records: [ + { skyflowID: 'ok1', tableName: 'table1', httpCode: 200, tokens: {} }, + { skyflowID: null, tableName: '', httpCode: 400, error: 'not found' }, + ], + }; + const res = constructFlowDBInsertResponse(body); + expect(res).not.toHaveProperty('errors'); + expect(res.records).toHaveLength(2); + expect(res.records[0]).toEqual({ + tableName: 'table1', skyflowId: 'ok1', tokens: {}, httpCode: 200, + }); + expect(res.records[1]).toEqual({ + error: 'not found', tableName: '', httpCode: 400, + }); + }); + + test('includes hashedData only when non-empty', () => { + const body = { + records: [ + { skyflowID: 'a', tableName: 't', httpCode: 200, tokens: {}, hashedData: { ssn: [{ data: 'h', hashName: 'hash1' }] } }, + { skyflowID: 'b', tableName: 't', httpCode: 200, tokens: {}, hashedData: {} }, + { skyflowID: 'c', tableName: 't', httpCode: 200, tokens: {} }, + ], + }; + const res = constructFlowDBInsertResponse(body); + expect(res.records[0].hashedData).toEqual({ ssn: [{ data: 'h', hashName: 'hash1' }] }); + expect(res.records[1]).not.toHaveProperty('hashedData'); + expect(res.records[2]).not.toHaveProperty('hashedData'); + }); +}); + +describe('constructFlowDBInsertError', () => { + test('passes through raw API error body when present on error.data', () => { + const out = constructFlowDBInsertError({ + data: { + error: { + httpCode: 404, + message: 'Vault not found.', + httpStatus: 'Not Found', + details: [], + }, + }, + }); + expect(out).toEqual({ + error: { + httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }); + }); + + test('normalizes a snake_case API error body to camelCase', () => { + const out = constructFlowDBInsertError({ + data: { + error: { + grpc_code: 5, + http_code: 404, + message: 'Vault not found.', + http_status: 'Not Found', + details: [], + }, + }, + }); + expect(out).toEqual({ + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }); + }); + + test('falls back to SkyflowError code/description when no raw body', () => { + const out = constructFlowDBInsertError({ error: { code: 500, description: 'boom', type: 'INTERNAL_SERVER_ERROR' } }); + expect(out).toEqual({ + error: { httpCode: 500, message: 'boom' }, + }); + }); +}); + +describe('constructFlowDBUpdateRequest', () => { + const finalUpdateRecords = { + updateRecords: [ + { table: 'table1', skyflowID: 'id1', fields: { name: 'Vivek', table: 'table1', skyflowID: 'id1' } }, + ], + }; + + test('defaults options to { tokens: true } when omitted (no updateType)', () => { + const req = constructFlowDBUpdateRequest(finalUpdateRecords, undefined, 'vault123'); + expect(req.records[0]).toEqual({ skyflowID: 'id1', tableName: 'table1', data: { name: 'Vivek' } }); + expect(req.records[0].updateType).toBeUndefined(); + }); + + test('maps to flowDB update shape, omitting table/skyflowID from data', () => { + const req = constructFlowDBUpdateRequest(finalUpdateRecords, { tokens: true }, 'vault123'); + expect(req).toEqual({ + vaultID: 'vault123', + records: [ + { skyflowID: 'id1', tableName: 'table1', data: { name: 'Vivek' } }, + ], + }); + }); + + test('sources updateType per-record from the matching table upsert entry', () => { + const req = constructFlowDBUpdateRequest( + finalUpdateRecords, + { upsert: [{ tableName: 'table1', uniqueColumns: ['name'], updateType: 'REPLACE' }] }, + 'vault123', + ); + expect(req.records[0].updateType).toBe('REPLACE'); + }); + + test('omits updateType when the record table has no upsert entry', () => { + const req = constructFlowDBUpdateRequest( + finalUpdateRecords, + { upsert: [{ tableName: 'other', uniqueColumns: ['name'], updateType: 'REPLACE' }] }, + 'vault123', + ); + expect(req.records[0].updateType).toBeUndefined(); + }); +}); + +describe('additionalFields (AdditionalFields) → flowDB request bodies', () => { + test('passes element req/update through unchanged when no additionalFields are supplied', () => { + const req = { table1: { ssn: '999' } }; + const update = { id1: { name: 'V', table: 'table1' } }; + const [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq(req, update, {}); + expect(finalInsertRecords.records).toEqual([{ table: 'table1', fields: { ssn: '999' } }]); + expect(finalUpdateRecords.updateRecords).toEqual([ + { table: 'table1', fields: { name: 'V', table: 'table1' }, skyflowID: 'id1' }, + ]); + }); + + test('records without skyflowId become inserts (tableName/data) in the flowDB insert body', () => { + const options = { + additionalFields: { + records: [ + { tableName: 'table1', data: { ssn: '999' } }, + ], + }, + }; + const [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq({}, {}, options); + const insertReq = constructFlowDBInsertRequest(finalInsertRecords, options, 'vault123'); + + expect(insertReq).toEqual({ + vaultID: 'vault123', + records: [{ tableName: 'table1', data: { ssn: '999' } }], + }); + expect(finalUpdateRecords.updateRecords).toHaveLength(0); + }); + + test('records with top-level skyflowId become updates (skyflowID/tableName/data) in the flowDB update body', () => { + const options = { + additionalFields: { + records: [ + { tableName: 'table1', data: { name: 'Vivek' }, skyflowId: 'id1' }, + ], + }, + }; + const [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq({}, {}, options); + const updateReq = constructFlowDBUpdateRequest(finalUpdateRecords, { tokens: true }, 'vault123'); + + expect(finalInsertRecords.records).toHaveLength(0); + expect(updateReq).toEqual({ + vaultID: 'vault123', + records: [{ skyflowID: 'id1', tableName: 'table1', data: { name: 'Vivek' } }], + }); + }); + + test('merges an additionalFields record into an existing update id (same skyflowId)', () => { + const options = { + additionalFields: { + records: [{ tableName: 'table1', data: { newCol: 'y' }, skyflowId: 'id1' }], + }, + }; + // `update` already carries id1 (from an element with that skyflowID), so the + // additionalFields record merges into it rather than creating a new entry. + const update = { id1: { existingCol: 'x', table: 'table1' } }; + const [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq({}, update, options); + + expect(finalInsertRecords.records).toHaveLength(0); + expect(finalUpdateRecords.updateRecords).toEqual([ + { table: 'table1', fields: { newCol: 'y', existingCol: 'x', table: 'table1' }, skyflowID: 'id1' }, + ]); + }); + + test('merges an additionalFields record into an existing insert table (same tableName)', () => { + const options = { + additionalFields: { + records: [{ tableName: 'table1', data: { newCol: 'y' } }], + }, + }; + // `req` already carries table1 (from an element on that table), so the + // additionalFields record merges into it rather than creating a new entry. + const req = { table1: { existingCol: 'x' } }; + const [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq(req, {}, options); + + expect(finalUpdateRecords.updateRecords).toHaveLength(0); + expect(finalInsertRecords.records).toEqual([ + { table: 'table1', fields: { newCol: 'y', existingCol: 'x' } }, + ]); + }); + + test('does not pollute Object.prototype when the merge source carries a __proto__ key', () => { + const options = { + additionalFields: { + records: [{ tableName: 'table1', data: { newCol: 'y' } }], + }, + }; + // Collected element data (the merge source) carries a malicious __proto__ + // key as an own property, as it would after JSON parsing. + const req = { table1: JSON.parse('{"existingCol":"x","__proto__":{"polluted":"yes"}}') }; + const [finalInsertRecords] = constructElementsInsertReq(req, {}, options); + + expect(({}).polluted).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + // Legitimate fields still merge; the forbidden key is dropped. + expect(finalInsertRecords.records).toEqual([ + { table: 'table1', fields: { newCol: 'y', existingCol: 'x' } }, + ]); + delete Object.prototype.polluted; + }); + + test('mixes inserts and skyflowId updates in a single additionalFields batch', () => { + const options = { + additionalFields: { + records: [ + { tableName: 'table1', data: { ssn: '999' } }, + { tableName: 'table2', data: { name: 'Vivek' }, skyflowId: 'id2' }, + ], + }, + }; + const [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq({}, {}, options); + + expect(constructFlowDBInsertRequest(finalInsertRecords, options, 'vault123').records) + .toEqual([{ tableName: 'table1', data: { ssn: '999' } }]); + expect(constructFlowDBUpdateRequest(finalUpdateRecords, {}, 'vault123').records) + .toEqual([{ skyflowID: 'id2', tableName: 'table2', data: { name: 'Vivek' } }]); + }); +}); + +describe('insertDataInCollectFlowDB', () => { + const finalInsertRecords = { records: [{ table: 'table1', fields: { ssn: '999' } }] }; + + test('resolves with parsed { records, errors } on success', async () => { + const client = buildClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + records: [{ skyflowID: 'id1', tableName: 'table1', httpCode: 200, tokens: { ssn: [{ token: 't1', tokenGroupName: 'det' }] } }], + }); + const out = await insertDataInCollectFlowDB(finalInsertRecords, client, 'auth-token'); + expect(out).toEqual({ + records: [{ + tableName: 'table1', skyflowId: 'id1', tokens: { ssn: [{ token: 't1', tokenGroupName: 'det' }] }, httpCode: 200, + }], + }); + }); + + test('always resolves with { error } on request failure', async () => { + const client = buildClient(); + jest.spyOn(client, 'request').mockRejectedValue({ error: { code: 500, description: 'insert failed' } }); + const out = await insertDataInCollectFlowDB(finalInsertRecords, client, 'auth-token'); + expect(out).toEqual({ + error: { httpCode: 500, message: 'insert failed' }, + }); + expect(out.records).toBeUndefined(); + }); + + test('resolves as success when a rejected body carries a records key (partial failure)', async () => { + const client = buildClient(); + jest.spyOn(client, 'request').mockRejectedValue({ + error: { code: 400, description: 'partial failure' }, + data: { + records: [ + { skyflowID: 'id1', tableName: 'table1', httpCode: 200, tokens: { ssn: [{ token: 't1', tokenGroupName: 'det' }] } }, + { skyflowID: null, tableName: '', httpCode: 400, error: 'not found' }, + ], + }, + }); + const out = await insertDataInCollectFlowDB(finalInsertRecords, client, 'auth-token'); + expect(out).not.toHaveProperty('error'); + expect(out.records).toEqual([ + { + tableName: 'table1', skyflowId: 'id1', tokens: { ssn: [{ token: 't1', tokenGroupName: 'det' }] }, httpCode: 200, + }, + { error: 'not found', tableName: '', httpCode: 400 }, + ]); + }); +}); + +describe('updateDataInCollectFlowDB', () => { + const finalUpdateRecords = { + updateRecords: [{ table: 'table1', skyflowID: 'id1', fields: { name: 'V', table: 'table1', skyflowID: 'id1' } }], + }; + + test('resolves with parsed { records, errors } on success', async () => { + const client = buildClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + records: [{ skyflowID: 'id1', tableName: 'table1', httpCode: 200, tokens: { name: [{ token: 't1', tokenGroupName: 'det' }] } }], + }); + const out = await updateDataInCollectFlowDB(finalUpdateRecords, client, 'auth-token'); + expect(out).toEqual({ + records: [{ + tableName: 'table1', skyflowId: 'id1', tokens: { name: [{ token: 't1', tokenGroupName: 'det' }] }, httpCode: 200, + }], + }); + }); + + test('always resolves with { error } on request failure', async () => { + const client = buildClient(); + jest.spyOn(client, 'request').mockRejectedValue({ error: { code: 400, description: 'update failed' } }); + const out = await updateDataInCollectFlowDB(finalUpdateRecords, client, 'auth-token'); + expect(out).toEqual({ + error: { httpCode: 400, message: 'update failed' }, + }); + }); + + test('resolves as success when a rejected body carries a records key (partial failure)', async () => { + const client = buildClient(); + jest.spyOn(client, 'request').mockRejectedValue({ + error: { code: 400, description: 'partial failure' }, + data: { + records: [ + { skyflowID: 'id1', tableName: 'table1', httpCode: 200, tokens: { name: [{ token: 't1', tokenGroupName: 'det' }] } }, + ], + }, + }); + const out = await updateDataInCollectFlowDB(finalUpdateRecords, client, 'auth-token'); + expect(out).not.toHaveProperty('error'); + expect(out.records).toEqual([ + { + tableName: 'table1', skyflowId: 'id1', tokens: { name: [{ token: 't1', tokenGroupName: 'det' }] }, httpCode: 200, + }, + ]); + }); +}); + +describe('mergeFlowDBCollectResponses (mixed insert/update outcomes)', () => { + const emptyCvvMap = { insert: {}, update: {} }; + + const successRecord = { + tableName: 'table1', + skyflowId: 'id1', + tokens: { card_number: [{ token: 't1', tokenGroupName: 'det' }] }, + httpCode: 200, + }; + + test('both endpoints succeed → resolve shape merges all records, no error record', () => { + const insertOk = { records: [{ tableName: 'table2', tokens: {}, httpCode: 200 }] }; + const updateOk = { records: [successRecord] }; + const out = mergeFlowDBCollectResponses([insertOk, updateOk], emptyCvvMap); + expect(out).toEqual({ + records: [ + { tableName: 'table2', tokens: {}, httpCode: 200 }, + successRecord, + ], + }); + expect(out).not.toHaveProperty('error'); + }); + + test('one endpoint fully fails, sibling returns records → resolve with surviving records + inline error record', () => { + const insertFail = { error: { httpCode: 401, message: 'invalid token' } }; + const updateOk = { records: [successRecord] }; + const out = mergeFlowDBCollectResponses([insertFail, updateOk], emptyCvvMap); + expect(out).not.toHaveProperty('error'); + expect(out.records).toEqual([ + successRecord, + { error: 'invalid token', httpCode: 401 }, + ]); + }); + + test('inline error record omits httpCode when the error envelope has no numeric code', () => { + const insertFail = { error: { httpStatus: 'UNAUTHENTICATED', message: 'no numeric code' } }; + const updateOk = { records: [successRecord] }; + const out = mergeFlowDBCollectResponses([insertFail, updateOk], emptyCvvMap); + expect(out.records[1]).toEqual({ error: 'no numeric code' }); + expect(out.records[1]).not.toHaveProperty('httpCode'); + }); + + test('inline error record uses empty string when the error envelope has no message', () => { + const insertFail = { error: { httpCode: 500 } }; + const updateOk = { records: [successRecord] }; + const out = mergeFlowDBCollectResponses([insertFail, updateOk], emptyCvvMap); + expect(out.records[1]).toEqual({ error: '', httpCode: 500 }); + }); + + test('every endpoint fully fails (nothing landed) → returns the first { error } for the caller to reject on', () => { + const insertFail = { error: { httpCode: 401, message: 'insert failed' } }; + const updateFail = { error: { httpCode: 400, message: 'update failed' } }; + const out = mergeFlowDBCollectResponses([insertFail, updateFail], emptyCvvMap); + expect(out).toEqual({ error: { httpCode: 401, message: 'insert failed' } }); + expect(out).not.toHaveProperty('records'); + }); + + test('single endpoint full failure → returns { error } (unchanged reject path)', () => { + const insertFail = { error: { httpCode: 401, message: 'insert failed' } }; + const out = mergeFlowDBCollectResponses([insertFail], emptyCvvMap); + expect(out).toEqual({ error: { httpCode: 401, message: 'insert failed' } }); + }); + + test('applies CVV mock to surviving success records in a mixed outcome', () => { + const insertFail = { error: { httpCode: 401, message: 'invalid token' } }; + const updateOk = { + records: [{ + tableName: 'table1', + skyflowId: 'id1', + tokens: { cvv: [{ token: 'real-cvv-token' }] }, + httpCode: 200, + }], + }; + const cvvMap = { insert: {}, update: { id1: { cvv: '123' } } }; + const out = mergeFlowDBCollectResponses([insertFail, updateOk], cvvMap); + // token replaced with a 3-char mock (never the entered value), error record appended + expect(out.records[0].tokens.cvv[0].token).not.toBe('real-cvv-token'); + expect(out.records[0].tokens.cvv[0].token).toHaveLength(3); + expect(out.records[1]).toEqual({ error: 'invalid token', httpCode: 401 }); + }); +}); + +describe('replaceCVVTokensInResponse', () => { + const emptyCvvMap = { insert: {}, update: {} }; + + test('returns records unchanged when records is falsy', () => { + expect(replaceCVVTokensInResponse(undefined, emptyCvvMap)).toBeUndefined(); + expect(replaceCVVTokensInResponse(null, emptyCvvMap)).toBeNull(); + }); + + test('skips a record with no tokens (and a null record)', () => { + const records = [null, { tableName: 't1', httpCode: 200 }]; + expect(replaceCVVTokensInResponse(records, { insert: { t1: { cvv: '123' } }, update: {} })) + .toBe(records); + expect(records[1]).toEqual({ tableName: 't1', httpCode: 200 }); + }); + + test('leaves tokens untouched when no columnMap matches the record', () => { + const records = [{ tableName: 't1', tokens: { cvv: 'real' } }]; + replaceCVVTokensInResponse(records, { insert: { other: { cvv: '123' } }, update: {} }); + expect(records[0].tokens.cvv).toBe('real'); + }); + + test('insert path: replaces a flat primitive token with the length-matched mock', () => { + const records = [{ tableName: 't1', tokens: { cvv: 'real-token' } }]; + replaceCVVTokensInResponse(records, { insert: { t1: { cvv: '123' } }, update: {} }); + expect(records[0].tokens.cvv).toBe('817'); + }); + + test('update path: replaces the token inside a non-array object token value', () => { + const records = [{ skyflowId: 'id1', tokens: { cvv: { token: 'real-token' } } }]; + replaceCVVTokensInResponse(records, { insert: {}, update: { id1: { cvv: '1234' } } }); + expect(records[0].tokens.cvv.token).toBe('8173'); + }); + + test('flat array column: replaces only the path-less entries', () => { + const records = [{ + tableName: 't1', + tokens: { cvv: [{ token: 'a' }, { token: 'b', path: 'sub' }] }, + }]; + replaceCVVTokensInResponse(records, { insert: { t1: { cvv: '123' } }, update: {} }); + expect(records[0].tokens.cvv[0].token).toBe('817'); + expect(records[0].tokens.cvv[1].token).toBe('b'); + }); + + test('nested array column: replaces only the entry whose path exactly matches', () => { + const records = [{ + tableName: 't1', + tokens: { address: [{ token: 'a', path: 'city' }, { token: 'b', path: 'ward' }] }, + }]; + replaceCVVTokensInResponse(records, { insert: { t1: { 'address.city': '123' } }, update: {} }); + expect(records[0].tokens.address[0].token).toBe('817'); + expect(records[0].tokens.address[1].token).toBe('b'); + }); + + test('skips array entries that are not token-bearing objects', () => { + const records = [{ + tableName: 't1', + tokens: { cvv: [null, 'str', { noToken: 1 }, { token: 'x' }] }, + }]; + replaceCVVTokensInResponse(records, { insert: { t1: { cvv: '123' } }, update: {} }); + expect(records[0].tokens.cvv).toEqual([null, 'str', { noToken: 1 }, { token: '817' }]); + }); + + test('skips a mapped column that is absent from the token map', () => { + const records = [{ tableName: 't1', tokens: { other: 'keep' } }]; + replaceCVVTokensInResponse(records, { insert: { t1: { cvv: '123' } }, update: {} }); + expect(records[0].tokens.other).toBe('keep'); + }); + + test('uses an empty-string mock when the entered value is empty', () => { + const records = [{ tableName: 't1', tokens: { cvv: 'real' } }]; + replaceCVVTokensInResponse(records, { insert: { t1: { cvv: '' } }, update: {} }); + expect(records[0].tokens.cvv).toBe(''); + }); + + test('leaves a nested-path column untouched when its top-level token value is not an array', () => { + // nestedPath is defined ('city') but tokens.address is a plain object, not an + // array of path-bearing entries, so nothing is replaced. + const records = [{ tableName: 't1', tokens: { address: { token: 'keep' } } }]; + replaceCVVTokensInResponse(records, { insert: { t1: { 'address.city': '123' } }, update: {} }); + expect(records[0].tokens.address).toEqual({ token: 'keep' }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/api-utils/reveal.flowdb.test.js b/packages/skyflow-flowvault-js/tests/api-utils/reveal.flowdb.test.js new file mode 100644 index 000000000..37849f5f8 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/api-utils/reveal.flowdb.test.js @@ -0,0 +1,615 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import { + constructFlowDBDetokenizeRequest, + constructFlowDBDetokenizeResponse, + constructFlowDBDetokenizeError, + formatRecordsForClientFlowDB, + formatRecordsForClientComposableFlowDB, + fetchRecordsByTokenIdFlowDB, + fetchRecordsByTokenIdComposableFlowDB, +} from '../../src/api-utils/reveal'; +import { Env, LogLevel, RedactionType } from '../../src/utils/common'; +import Client from '@core/client'; +import { getAccessToken } from '@core/utils/bus-events'; + +// flowvault's reveal data layer imports getAccessToken from @core/utils/bus-events +// (jest applies moduleNameMapper to jest.mock paths, so mock the @core path). +jest.mock('@core/utils/bus-events', () => ({ + getAccessToken: jest.fn(() => Promise.resolve('mockAccessToken')), +})); + +afterEach(() => { + getAccessToken.mockClear(); + getAccessToken.mockImplementation(() => Promise.resolve('mockAccessToken')); +}); + +const skyflowConfig = { + vaultID: 'vault123', + vaultURL: 'https://testurl.com', + getBearerToken: jest.fn(), +}; + +const clientJSON = { + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, + config: { ...skyflowConfig, getBearerToken: jest.fn().toString() }, + metaData: { uuid: 'id' }, +}; + +const makeClient = () => Client.fromJSON(clientJSON); + +describe('constructFlowDBDetokenizeRequest', () => { + it('sends only vaultID and tokens when no redaction info is present', () => { + const records = [{ token: 'token1' }, { token: 'token2' }]; + const req = constructFlowDBDetokenizeRequest(records, 'vault123'); + expect(req).toEqual({ vaultID: 'vault123', tokens: ['token1', 'token2'] }); + expect(req.tokenGroupRedactions).toBeUndefined(); + }); + + it('uses tokenGroupRedactions from reveal options when provided', () => { + const records = [{ token: 'token1' }]; + const tokenGroupRedactions = [ + { tokenGroupName: 'det_reg_rtf', redaction: RedactionType.PLAIN_TEXT }, + ]; + const req = constructFlowDBDetokenizeRequest(records, 'vault123', { tokenGroupRedactions }); + expect(req).toEqual({ vaultID: 'vault123', tokens: ['token1'], tokenGroupRedactions }); + }); + + it('forwards multiple tokenGroupRedactions entries verbatim from options', () => { + const records = [{ token: 'token1' }, { token: 'token2' }]; + const tokenGroupRedactions = [ + { tokenGroupName: 'grp1', redaction: RedactionType.MASKED }, + { tokenGroupName: 'grp2', redaction: 'CUSTOM_REDACTION' }, + ]; + const req = constructFlowDBDetokenizeRequest(records, 'vault123', { tokenGroupRedactions }); + expect(req.tokens).toEqual(['token1', 'token2']); + expect(req.tokenGroupRedactions).toEqual(tokenGroupRedactions); + }); + + it('ignores element-level tokenGroupName/redaction (no longer a request source)', () => { + const records = [ + { token: 'token1', tokenGroupName: 'grp1', redaction: RedactionType.MASKED }, + ]; + const req = constructFlowDBDetokenizeRequest(records, 'vault123'); + expect(req).toEqual({ vaultID: 'vault123', tokens: ['token1'] }); + expect(req.tokenGroupRedactions).toBeUndefined(); + }); + + it('omits tokenGroupRedactions when options provide an empty array', () => { + const records = [{ token: 'token1' }]; + const req = constructFlowDBDetokenizeRequest(records, 'vault123', { tokenGroupRedactions: [] }); + expect(req).toEqual({ vaultID: 'vault123', tokens: ['token1'] }); + expect(req.tokenGroupRedactions).toBeUndefined(); + }); +}); + +describe('constructFlowDBDetokenizeResponse', () => { + it('splits response into records (with tokenGroupName + httpCode) and inline errors', () => { + const responseBody = { + response: [ + { token: 'token1', value: 'Bata Gali', tokenGroupName: 'nondet_reg', error: null, httpCode: 200 }, + { token: 'dummy', value: null, tokenGroupName: null, error: 'token not found', httpCode: 500 }, + ], + }; + const result = constructFlowDBDetokenizeResponse(responseBody); + expect(result.records).toEqual([ + { token: 'token1', value: 'Bata Gali', tokenGroupName: 'nondet_reg', httpCode: 200 }, + ]); + expect(result.records[0].valueType).toBeUndefined(); + expect(result.errors).toEqual([ + { token: 'dummy', error: { code: 500, description: 'token not found' } }, + ]); + }); + + it('omits tokenGroupName when it is null/absent', () => { + const responseBody = { + response: [ + { token: 't1', value: 'a', tokenGroupName: null, httpCode: 200 }, + { token: 't2', value: 'b', httpCode: 200 }, + ], + }; + const result = constructFlowDBDetokenizeResponse(responseBody); + expect(result.records[0].tokenGroupName).toBeUndefined(); + expect(result.records[1].tokenGroupName).toBeUndefined(); + }); + + it('preserves non-string values', () => { + const responseBody = { + response: [{ token: 't', value: [9087, 6543], error: null, httpCode: 200 }], + }; + const result = constructFlowDBDetokenizeResponse(responseBody); + expect(result.records[0].value).toEqual([9087, 6543]); + }); + + it('includes metadata only when it is a non-empty object', () => { + const responseBody = { + response: [ + { token: 't1', value: 'a', metadata: { table: 'persons', skyflowID: 'id1' }, httpCode: 200 }, + { token: 't2', value: 'b', metadata: {}, httpCode: 200 }, + { token: 't3', value: 'c', httpCode: 200 }, + ], + }; + const result = constructFlowDBDetokenizeResponse(responseBody); + expect(result.records[0].metadata).toEqual({ table: 'persons', skyflowID: 'id1' }); + expect(result.records[1].metadata).toBeUndefined(); + expect(result.records[2].metadata).toBeUndefined(); + }); + + it('handles an empty/absent response array', () => { + expect(constructFlowDBDetokenizeResponse({})).toEqual({ records: [], errors: [] }); + expect(constructFlowDBDetokenizeResponse({ response: [] })).toEqual({ records: [], errors: [] }); + }); +}); + +describe('constructFlowDBDetokenizeError', () => { + it('passes the raw API error body through when present on error.data', () => { + const err = { + data: { + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }, + error: { code: 404, description: 'Vault not found.' }, + }; + expect(constructFlowDBDetokenizeError(err).error).toEqual({ + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }); + }); + + it('normalizes a snake_case API error body to camelCase', () => { + const err = { + data: { + error: { + grpc_code: 5, + http_code: 404, + message: 'Vault not found.', + http_status: 'Not Found', + details: [], + }, + }, + error: { code: 404, description: 'Vault not found.' }, + }; + expect(constructFlowDBDetokenizeError(err).error).toEqual({ + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }); + }); + + it('falls back to httpCode/message from SkyflowError when no raw body', () => { + const err = { error: { code: 500, description: 'network error' } }; + const out = constructFlowDBDetokenizeError(err); + expect(out.error).toEqual({ httpCode: 500, message: 'network error' }); + expect(out.errors).toEqual([{ token: '', error: { code: 500, description: 'network error' } }]); + }); + + it('normalizes error.data itself when data carries no nested error key', () => { + const err = { + data: { + grpc_code: 5, http_code: 500, message: 'raw body, no error key', + }, + error: { code: 500, description: 'raw body, no error key' }, + }; + expect(constructFlowDBDetokenizeError(err).error).toEqual({ + grpcCode: 5, httpCode: 500, message: 'raw body, no error key', + }); + }); +}); + +describe('formatRecordsForClientFlowDB', () => { + it('builds unified records: success carries token/tokenGroupName/metadata/httpCode, no value', () => { + const response = { + records: [ + { token: 't1', value: 'a', httpCode: 200 }, + { + token: 't2', value: 'b', tokenGroupName: 'nondet_reg', metadata: { table: 'persons', skyflowID: 'id1' }, httpCode: 200, + }, + ], + }; + expect(formatRecordsForClientFlowDB(response)).toEqual({ + records: [ + { token: 't1', httpCode: 200 }, + { + token: 't2', tokenGroupName: 'nondet_reg', metadata: { tableName: 'persons', skyflowId: 'id1' }, httpCode: 200, + }, + ], + }); + }); + + it('inlines per-record errors into records as { error, token, httpCode }', () => { + const response = { + records: [{ token: 'ok', value: 'v', httpCode: 200 }], + errors: [{ token: 'bad', error: { code: 404, description: 'invalid token' } }], + }; + expect(formatRecordsForClientFlowDB(response)).toEqual({ + records: [ + { token: 'ok', httpCode: 200 }, + { error: 'invalid token', token: 'bad', httpCode: 404 }, + ], + }); + }); + + it('passes a full-failure raw body through as a top-level { error }', () => { + const response = { + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }; + expect(formatRecordsForClientFlowDB(response)).toEqual({ + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }); + }); + + it('handles a response with only inline errors (no records key)', () => { + const response = { errors: [{ token: 'bad', error: { code: 404, description: 'nf' } }] }; + expect(formatRecordsForClientFlowDB(response)).toEqual({ + records: [{ error: 'nf', token: 'bad', httpCode: 404 }], + }); + }); + + it('uses the raw error when an inline error carries no description', () => { + const response = { records: [], errors: [{ token: 'bad', error: 'flat error string' }] }; + expect(formatRecordsForClientFlowDB(response).records[0].error).toBe('flat error string'); + }); + + it('keeps a tokenGroupName-only record and skips empty metadata', () => { + const response = { + records: [ + { token: 't1', tokenGroupName: 'grp', metadata: {}, httpCode: 200 }, + ], + }; + expect(formatRecordsForClientFlowDB(response)).toEqual({ + records: [{ token: 't1', tokenGroupName: 'grp', httpCode: 200 }], + }); + }); + + it('normalizes metadata that lacks table/skyflowID (leaves other keys intact)', () => { + const response = { + records: [{ token: 't1', metadata: { region: 'us' }, httpCode: 200 }], + }; + expect(formatRecordsForClientFlowDB(response)).toEqual({ + records: [{ token: 't1', metadata: { region: 'us' }, httpCode: 200 }], + }); + }); +}); + +describe('formatRecordsForClientComposableFlowDB', () => { + it('builds unified records from index-0 shape with httpCode, no value', () => { + const response = { records: [{ 0: { token: 't1', value: 'a', httpCode: 200 }, frameId: 'f1' }] }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + records: [{ token: 't1', httpCode: 200 }], + }); + }); + + it('inlines errors into records and keeps successes', () => { + const response = { + records: [{ 0: { token: 't1', value: 'a', httpCode: 200 }, frameId: 'f1' }], + errors: [{ token: 't2', error: { code: 404, description: 'nf' }, frameId: 'f2' }], + }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + records: [ + { token: 't1', httpCode: 200 }, + { error: 'nf', token: 't2', httpCode: 404 }, + ], + }); + }); + + it('normalizes metadata keys (table -> tableName, skyflowID -> skyflowId)', () => { + const response = { + records: [{ + 0: { + token: 't1', value: 'a', metadata: { table: 'persons', skyflowID: 'id1' }, httpCode: 200, + }, + frameId: 'f1', + }], + }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + records: [{ token: 't1', metadata: { tableName: 'persons', skyflowId: 'id1' }, httpCode: 200 }], + }); + }); + + it('passes a full-failure raw body through as a top-level { error }', () => { + const response = { error: { httpCode: 404, message: 'Vault not found.' } }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + error: { httpCode: 404, message: 'Vault not found.' }, + }); + }); + + it('handles a response with only errors (no records key) and flat error strings', () => { + const response = { errors: [{ error: 'flat error' }] }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + records: [{ error: 'flat error', token: '', httpCode: undefined }], + }); + }); + + it('defaults token to empty string when a record has no index-0 payload', () => { + const response = { records: [{ frameId: 'f1' }] }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + records: [{ token: '', httpCode: undefined }], + }); + }); + + it('keeps a tokenGroupName-only record and skips empty metadata', () => { + const response = { + records: [{ 0: { token: 't1', tokenGroupName: 'grp', metadata: {}, httpCode: 200 }, frameId: 'f1' }], + }; + expect(formatRecordsForClientComposableFlowDB(response)).toEqual({ + records: [{ token: 't1', tokenGroupName: 'grp', httpCode: 200 }], + }); + }); +}); + +describe('fetchRecordsByTokenIdFlowDB', () => { + it('issues a single batch POST to /v2/tokens/detokenize and resolves records', async () => { + const client = makeClient(); + const requestSpy = jest.spyOn(client, 'request').mockResolvedValue({ + response: [ + { token: 'token1', value: 'val1', tokenGroupName: 'nondet_reg', httpCode: 200 }, + { token: 'token2', value: 'val2', httpCode: 200 }, + ], + }); + + const result = await fetchRecordsByTokenIdFlowDB( + [{ token: 'token1' }, { token: 'token2' }], client, + ); + + expect(requestSpy).toHaveBeenCalledTimes(1); + const call = requestSpy.mock.calls[0][0]; + expect(call.requestMethod).toBe('POST'); + expect(call.url).toBe('https://testurl.com/v2/tokens/detokenize'); + expect(JSON.parse(call.body)).toEqual({ vaultID: 'vault123', tokens: ['token1', 'token2'] }); + expect(result).toEqual({ + records: [ + { token: 'token1', value: 'val1', tokenGroupName: 'nondet_reg', httpCode: 200 }, + { token: 'token2', value: 'val2', httpCode: 200 }, + ], + }); + }); + + it('rejects with only errors when every token fails inline', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 'token1', value: null, error: 'not found', httpCode: 404 }], + }); + + await expect(fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client)) + .rejects.toEqual({ + errors: [{ token: 'token1', error: { code: 404, description: 'not found' } }], + }); + }); + + it('rejects with records and errors on partial success', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [ + { token: 'ok', value: 'v', httpCode: 200 }, + { token: 'bad', value: null, error: 'not found', httpCode: 404 }, + ], + }); + + await expect( + fetchRecordsByTokenIdFlowDB([{ token: 'ok' }, { token: 'bad' }], client), + ).rejects.toEqual({ + records: [{ token: 'ok', value: 'v', httpCode: 200 }], + errors: [{ token: 'bad', error: { code: 404, description: 'not found' } }], + }); + }); + + it('rejects a request-level failure (no body) as a top-level { error }', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockRejectedValue({ + error: { code: 500, description: 'network error' }, + }); + + await expect(fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client)) + .rejects.toEqual({ + error: { httpCode: 500, message: 'network error' }, + }); + }); + + it('keeps httpCode on resolved success records', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 'token1', value: 'val1', httpCode: 200 }], + }); + + const result = await fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client); + expect(result).toEqual({ records: [{ token: 'token1', value: 'val1', httpCode: 200 }] }); + }); + + it('rejects a full failure as a top-level { error }', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockRejectedValue({ + data: { + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }, + error: { code: 404, description: 'Vault not found.' }, + }); + + await expect(fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client)) + .rejects.toEqual({ + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }); + }); + + it('routes a rejected body carrying a response array through the per-token success path', async () => { + const client = makeClient(); + // Non-2xx status but the body still has a `response` array (partial failure): + // per-token results/errors must flow to the client, not the top-level error. + jest.spyOn(client, 'request').mockRejectedValue({ + error: { code: 400, description: 'partial failure' }, + data: { + response: [ + { token: 'ok', value: 'v', httpCode: 200 }, + { token: 'bad', value: null, error: 'not found', httpCode: 404 }, + ], + }, + }); + + await expect(fetchRecordsByTokenIdFlowDB([{ token: 'ok' }, { token: 'bad' }], client)) + .rejects.toEqual({ + records: [{ token: 'ok', value: 'v', httpCode: 200 }], + errors: [{ token: 'bad', error: { code: 404, description: 'not found' } }], + }); + }); + + it('rejects with a top-level { error } when the request throws synchronously', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockImplementation(() => { throw new Error('sync boom'); }); + + await expect(fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client)) + .rejects.toHaveProperty('error'); + }); + + it('rejects when the access-token fetch fails', async () => { + const client = makeClient(); + const tokenError = { error: { code: 401, description: 'token fetch failed' } }; + getAccessToken.mockImplementationOnce(() => Promise.reject(tokenError)); + + await expect(fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client)) + .rejects.toEqual(tokenError); + }); + + it('falls back to an empty clientId when the client carries no uuid', async () => { + const client = Client.fromJSON({ ...clientJSON, metaData: {} }); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 'token1', value: 'val1', httpCode: 200 }], + }); + + const result = await fetchRecordsByTokenIdFlowDB([{ token: 'token1' }], client); + expect(getAccessToken).toHaveBeenCalledWith(''); + expect(result).toEqual({ records: [{ token: 'token1', value: 'val1', httpCode: 200 }] }); + }); +}); + +describe('fetchRecordsByTokenIdComposableFlowDB', () => { + it('re-attaches frameId per token and reshapes to index-0 records', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [ + { token: 'token1', value: 'val1', httpCode: 200 }, + { token: 'token2', value: 'val2', metadata: { table: 't' }, httpCode: 200 }, + ], + }); + + const records = [ + { token: 'token1', iframeName: 'frame1' }, + { token: 'token2', iframeName: 'frame2' }, + ]; + const result = await fetchRecordsByTokenIdComposableFlowDB(records, client, 'mockToken'); + + expect(client.request).toHaveBeenCalledTimes(1); + expect(result.records).toEqual([ + { 0: { token: 'token1', value: 'val1', httpCode: 200 }, frameId: 'frame1' }, + { + 0: { + token: 'token2', value: 'val2', metadata: { table: 't' }, httpCode: 200, + }, + frameId: 'frame2', + }, + ]); + }); + + it('rejects with errors carrying frameId when all tokens fail', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 'token1', error: 'not found', httpCode: 404 }], + }); + + await expect( + fetchRecordsByTokenIdComposableFlowDB([{ token: 'token1', iframeName: 'frame1' }], client, 'mockToken'), + ).rejects.toEqual({ + errors: [expect.objectContaining({ token: 'token1', frameId: 'frame1' })], + }); + }); + + it('rejects with both records and errors on partial success', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [ + { token: 'ok', value: 'v', httpCode: 200 }, + { token: 'bad', error: 'not found', httpCode: 404 }, + ], + }); + + const records = [ + { token: 'ok', iframeName: 'frame1' }, + { token: 'bad', iframeName: 'frame2' }, + ]; + await expect(fetchRecordsByTokenIdComposableFlowDB(records, client, 'mockToken')) + .rejects.toEqual({ + records: [{ 0: { token: 'ok', value: 'v', httpCode: 200 }, frameId: 'frame1' }], + errors: [expect.objectContaining({ token: 'bad', frameId: 'frame2' })], + }); + }); + + it('rejects with a top-level { error } raw body on a request-level failure', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockRejectedValue({ + data: { + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }, + error: { code: 404, description: 'Vault not found.' }, + }); + + await expect( + fetchRecordsByTokenIdComposableFlowDB([{ token: 'token1', iframeName: 'frame1' }], client, 'mockToken'), + ).rejects.toEqual({ + error: { + grpcCode: 5, httpCode: 404, message: 'Vault not found.', httpStatus: 'Not Found', details: [], + }, + }); + }); + + it('carries tokenGroupName through and defaults frameId to empty for an unmapped token', () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 'unmapped', value: 'v', tokenGroupName: 'grp', httpCode: 200 }], + }); + + return fetchRecordsByTokenIdComposableFlowDB( + [{ token: 'token1', iframeName: 'frame1' }], client, 'mockToken', + ).then((result) => { + expect(result.records).toEqual([ + { + 0: { + token: 'unmapped', value: 'v', tokenGroupName: 'grp', httpCode: 200, + }, + frameId: '', + }, + ]); + }); + }); + + it('defaults token/iframeName keys to empty string in the frameId map', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 't', value: 'v', httpCode: 200 }], + }); + + // Records with neither token nor iframeName exercise the `?? ''` fallbacks + // while the frame map is built. + const result = await fetchRecordsByTokenIdComposableFlowDB([{}], client, 'mockToken'); + expect(result.records[0].frameId).toBe(''); + }); + + it('defaults an error record frameId to empty for an unmapped token', async () => { + const client = makeClient(); + jest.spyOn(client, 'request').mockResolvedValue({ + response: [{ token: 'unmapped-err', value: null, error: 'not found', httpCode: 404 }], + }); + + await expect( + fetchRecordsByTokenIdComposableFlowDB([{ token: 'token1', iframeName: 'frame1' }], client, 'mockToken'), + ).rejects.toEqual({ + errors: [expect.objectContaining({ token: 'unmapped-err', frameId: '' })], + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/external/collect/collect-container.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/external/collect/collect-container.flowdb.test.ts new file mode 100644 index 000000000..fb67ff6e4 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/collect/collect-container.flowdb.test.ts @@ -0,0 +1,246 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB collect container tests. The shared collect()/create()/mount() mechanics +// are covered by the @core suite (skyflow-js tests/core); here we assert only the +// flowDB divergence injected into the subclass: create() accepts the client-facing +// `tableName` (remapped to `table`) and runs the flowDB collect-input validator, +// buildCreateElementFields validates the flowDB `returnMockValue` option, +// validateCollectOptions validates the flowDB upsert/additionalFields shapes and +// forces tokens on, and wrapCollectError maps a truthy error to SkyflowFlowDBError. +import { BaseElementType, FileElementType } from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import SkyflowError from '@core/errors'; +import CollectElement from '@core/external/collect/collect-element'; +import { + LogLevel, + Env, + ValidationRuleType, + CollectElementInput, + Context, +} from '../../../../src/utils/common'; +import CollectContainer from '../../../../src/external/collect/collect-container'; +import SkyflowFlowDBError from '../../../../src/libs/skyflow-flowdb-error'; +import collectVariant from '../../../../src/external/collect/collect-variant'; +import { ContainerType } from '../../../../src/skyflow'; +import { Metadata } from '../../../../src/internal/internal-types'; + +global.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +})); + +const bus = require('framebus'); + +jest.mock('@core/iframe-libs/iframer', () => { + const actualModule = jest.requireActual('@core/iframe-libs/iframer'); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => 'https://google.com'); + return mockedModule; +}); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); + +const mockUuid = '1234'; +jest.mock('@core/libs/uuid', () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +jest.mock('@core/external/collect/collect-element'); +(CollectElement as unknown as jest.Mock).mockImplementation(() => ({ + isMounted: () => true, + mount: jest.fn(), + isValidElement: () => true, + unmount: jest.fn(), + updateElementGroup: jest.fn(), +})); + +const metaData: Metadata = { + uuid: '123', + sdkVersion: '', + sessionId: '1234', + clientDomain: 'http://abc.com', + containerType: ContainerType.COLLECT, + clientJSON: { + config: { + vaultID: 'vault123', + vaultURL: 'https://sb.vault.dev', + getBearerToken, + }, + metaData: { + uuid: '123', + clientDomain: 'http://abc.com', + }, + }, + getSkyflowBearerToken: getBearerToken, + skyflowContainer: { + isControllerFrameReady: true, + } as any, +}; + +const context: Context = { logLevel: LogLevel.ERROR, env: Env.PROD }; + +const collectStylesOptions = { + inputStyles: { + cardIcon: { position: 'absolute', left: '8px', top: 'calc(50% - 10px)' }, + }, +}; + +// flowDB input uses the client-facing `tableName` key (remapped to `table`). +const cvvInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'primary_card.cvv', + placeholder: 'cvv', + label: 'cvv', + type: BaseElementType.CVV, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: { min: 2, max: 4, error: 'Error' }, + }, + ], + ...collectStylesOptions, +} as any; + +describe('flowDB collect container', () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + const on = jest.fn(); + + beforeEach(() => { + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + jest.spyOn(bus, 'on'); + targetSpy.mockReturnValue({ on, off: jest.fn(), emit: emitSpy }); + }); + + afterEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ''; + }); + + it('constructs a CollectContainer', () => { + const container = new CollectContainer(metaData, context); + expect(container).toBeInstanceOf(CollectContainer); + }); + + it('exposes the flowDB collectVariant (skyflowId key strategy)', () => { + const container = new CollectContainer(metaData, context); + expect((container as any).collectVariant).toBe(collectVariant); + expect((container as any).collectVariant.skyflowIdKey).toBe('skyflowId'); + }); + + // create() runs validateCreateInput (flowDB collect-input validator) and + // buildCreateElementFields (remaps tableName -> table, validates options). + describe('create()', () => { + it('accepts a flowDB (tableName) input and builds an element', () => { + const container = new CollectContainer(metaData, context); + const element = container.create(cvvInput); + expect(element).toBeDefined(); + }); + + it('validateCreateInput: throws when the element type is missing', () => { + const container = new CollectContainer(metaData, context); + expect(() => container.create({ tableName: 'cards', column: 'cvv' } as any)) + .toThrow(SkyflowError); + }); + + it('validateCreateInput: throws when skyflowId is not a string', () => { + const container = new CollectContainer(metaData, context); + expect(() => container.create({ + tableName: 'cards', column: 'cvv', type: BaseElementType.CVV, skyflowId: 123, + } as any)).toThrow(SkyflowError); + }); + + it('buildCreateElementFields: throws when returnMockValue is not a boolean', () => { + const container = new CollectContainer(metaData, context); + expect(() => container.create(cvvInput, { returnMockValue: 'yes' } as any)) + .toThrow(SkyflowError); + }); + + // flowDB has no file-element support; file types are rejected at create() rather + // than silently dropped by the collect pipeline. + it('validateCreateInput: rejects FILE_INPUT element type', () => { + const container = new CollectContainer(metaData, context); + expect(() => container.create({ + tableName: 'cards', column: 'file', type: FileElementType.FILE_INPUT, + } as any)).toThrow(SkyflowError); + }); + + it('validateCreateInput: rejects MULTI_FILE_INPUT element type', () => { + const container = new CollectContainer(metaData, context); + expect(() => container.create({ + tableName: 'cards', column: 'files', type: FileElementType.MULTI_FILE_INPUT, + } as any)).toThrow(SkyflowError); + }); + + // flowDB's documented identity key is `tableName`; a client-supplied `table` + // is rejected so collect and composable-collect behave identically. + it('validateCreateInput: rejects a client-supplied `table` key', () => { + const container = new CollectContainer(metaData, context); + expect(() => container.create({ + table: 'cards', column: 'cvv', type: BaseElementType.CVV, + } as any)).toThrow(SkyflowError); + }); + }); + + // validateCollectOptions is the seam that diverges from the @core privacyDB + // validators: it validates the flowDB upsert/additionalFields shapes and forces + // tokens on. Exercised directly, mirroring the composable-container suite. + describe('validateCollectOptions (flowDB shapes)', () => { + const container = new CollectContainer(metaData, context); + const validate = (options: any) => (container as any).validateCollectOptions(options); + + it('is a no-op passthrough (forces tokens on) with no upsert/additionalFields', () => { + expect(validate({})).toEqual({ tokens: true }); + }); + + it('accepts a flowDB upsert ({ tableName, uniqueColumns }) and forces tokens on', () => { + const options = { upsert: [{ tableName: 'cards', uniqueColumns: ['card_number'] }] }; + expect(validate(options)).toEqual({ ...options, tokens: true }); + }); + + it('accepts a flowDB additionalFields ({ tableName, data })', () => { + const options = { additionalFields: { records: [{ tableName: 'cards', data: { cvv: '123' } }] } }; + expect(validate(options).tokens).toBe(true); + }); + + it('rejects the privacyDB upsert shape ({ table, column })', () => { + expect(() => validate({ upsert: [{ table: 'cards', column: 'card_number' }] })) + .toThrow(SkyflowError); + }); + + it('rejects the privacyDB additionalFields shape ({ table, fields })', () => { + expect(() => validate({ additionalFields: { records: [{ table: 'cards', fields: { cvv: '1' } }] } })) + .toThrow(SkyflowError); + }); + }); + + // wrapCollectError maps a truthy error to SkyflowFlowDBError; a falsy error + // passes through unchanged (the deferred/no-error path). + describe('wrapCollectError', () => { + const container = new CollectContainer(metaData, context); + const wrap = (err: any) => (container as any).wrapCollectError(err); + + it('wraps a truthy error as SkyflowFlowDBError', () => { + expect(wrap({ http_code: 500, message: 'boom' })).toBeInstanceOf(SkyflowFlowDBError); + }); + + it('passes a falsy error through unchanged', () => { + expect(wrap(null)).toBeNull(); + expect(wrap(undefined)).toBeUndefined(); + }); + }); + + it('collect() rejects when no elements are added', (done) => { + const container = new CollectContainer(metaData, context); + container.collect().catch((err) => { + expect(err).toBeInstanceOf(SkyflowError); + expect(err.error.code).toBe(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT.code); + done(); + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/external/collect/collect-variant.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/external/collect/collect-variant.flowdb.test.ts new file mode 100644 index 000000000..7e4e58aca --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/collect/collect-variant.flowdb.test.ts @@ -0,0 +1,48 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB collectVariant adapter: remaps the client-facing skyflowId/tableName +// onto the internal skyflowID/table that the SET_VALUE handler consumes, and +// declares the id key it carries. +import collectVariant from '../../../../src/external/collect/collect-variant'; + +describe('flowDB collectVariant', () => { + test('exposes skyflowIdKey as "skyflowId"', () => { + expect(collectVariant.skyflowIdKey).toBe('skyflowId'); + }); + + describe('normalizeUpdateOptions', () => { + test('remaps both skyflowId -> skyflowID and tableName -> table', () => { + const options: any = { skyflowId: 'id1', tableName: 'cards', foo: 'bar' }; + collectVariant.normalizeUpdateOptions(options); + expect(options).toEqual({ skyflowID: 'id1', table: 'cards', foo: 'bar' }); + expect(options).not.toHaveProperty('skyflowId'); + expect(options).not.toHaveProperty('tableName'); + }); + + test('remaps only skyflowId when tableName is absent', () => { + const options: any = { skyflowId: 'id1' }; + collectVariant.normalizeUpdateOptions(options); + expect(options).toEqual({ skyflowID: 'id1' }); + }); + + test('remaps only tableName when skyflowId is absent', () => { + const options: any = { tableName: 'cards' }; + collectVariant.normalizeUpdateOptions(options); + expect(options).toEqual({ table: 'cards' }); + }); + + test('is a no-op when neither key is present', () => { + const options: any = { returnMockValue: true }; + collectVariant.normalizeUpdateOptions(options); + expect(options).toEqual({ returnMockValue: true }); + }); + + test('ignores inherited (non-own) skyflowId/tableName properties', () => { + const options: any = Object.create({ skyflowId: 'inherited', tableName: 'inherited' }); + collectVariant.normalizeUpdateOptions(options); + expect(options).not.toHaveProperty('skyflowID'); + expect(options).not.toHaveProperty('table'); + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/external/collect/composable-container.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/external/collect/composable-container.flowdb.test.ts new file mode 100644 index 000000000..0f6c42fac --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/collect/composable-container.flowdb.test.ts @@ -0,0 +1,352 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB composable collect container tests. The shared collect()/mount()/create() +// mechanics are covered by the @core suite (skyflow-js tests/core); here we assert +// the flowDB divergence: create() accepts the client-facing tableName, a full API +// failure is wrapped as SkyflowFlowDBError, and the factory returns the container. +import { + ELEMENT_EVENTS_TO_IFRAME, + BaseElementType, FileElementType, +} from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import SkyflowError from '@core/errors'; +import logs from '@core/utils/logs'; +import EventEmitter from '@core/event-emitter'; +import CollectElement from '@core/external/collect/collect-element'; +import properties from '@core/properties'; +import { + LogLevel, + Env, + ValidationRuleType, + CollectElementInput, + Context, + ICollectOptions, +} from '../../../../src/utils/common'; +import { CollectResponse } from '../../../../src/internal/internal-types'; +import ComposableContainer from '../../../../src/external/collect/compose-collect-container'; +import ComposableElement from '../../../../src/external/collect/compose-collect-element'; +import SkyflowFlowDBError from '../../../../src/libs/skyflow-flowdb-error'; +import { ContainerType } from '../../../../src/skyflow'; +import { Metadata } from '../../../../src/internal/internal-types'; + +global.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +})); + +const bus = require('framebus'); + +jest.mock('@core/iframe-libs/iframer', () => { + const actualModule = jest.requireActual('@core/iframe-libs/iframer'); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => 'https://google.com'); + return mockedModule; +}); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); + +const mockUuid = '1234'; +jest.mock('@core/libs/uuid', () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUnmount = jest.fn(); +const updateMock = jest.fn(); +jest.mock('@core/external/collect/collect-element'); + +(CollectElement as unknown as jest.Mock).mockImplementation( + (_, tempElements) => { + tempElements.rows[0].elements.forEach((element) => { + element.isMounted = true; + }); + return { + isMounted: () => true, + mount: jest.fn(), + isValidElement: () => true, + unmount: mockUnmount, + updateElement: updateMock, + }; + }, +); + +jest.mock('@core/event-emitter'); +const emitMock = jest.fn(); + +let emitterSpy: Function; +(EventEmitter as unknown as jest.Mock).mockImplementation(() => ({ + on: jest.fn().mockImplementation((name, cb) => { + emitterSpy = cb; + }), + _emit: emitMock, +})); + +const metaData: Metadata = { + uuid: '123', + sdkVersion: '', + sessionId: '1234', + clientDomain: 'http://abc.com', + containerType: ContainerType.COMPOSABLE, + clientJSON: { + config: { + vaultID: 'vault123', + vaultURL: 'https://sb.vault.dev', + getBearerToken, + }, + metaData: { + uuid: '123', + clientDomain: 'http://abc.com', + }, + }, + getSkyflowBearerToken: getBearerToken, + skyflowContainer: { + isControllerFrameReady: true, + } as any, +}; + +const collectStylesOptions = { + inputStyles: { + cardIcon: { + position: 'absolute', + left: '8px', + top: 'calc(50% - 10px)', + }, + }, +}; + +// flowDB input uses the client-facing `tableName` key (remapped to `table`). +const cvvElementInput: CollectElementInput = { + tableName: 'pii_fields', + column: 'primary_card.cvv', + placeholder: 'cvv', + label: 'cvv', + type: BaseElementType.CVV, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: { min: 2, max: 4, error: 'Error' }, + }, + ], + ...collectStylesOptions, +} as any; + +const cardNumberElement: CollectElementInput = { + tableName: 'pii_fields', + column: 'primary_card.card_number', + type: BaseElementType.CARD_NUMBER, + ...collectStylesOptions, +} as any; + +const context: Context = { logLevel: LogLevel.ERROR, env: Env.PROD }; + +const collectResponse: CollectResponse = { + records: [ + { + table: 'table', + fields: { + primary_card: { + card_number: 'token2', + cvv: 'token3', + }, + }, + }, + ], +} as any; + +describe('flowDB composable collect container', () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + const on = jest.fn(); + + beforeEach(() => { + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + jest.spyOn(bus, 'on'); + targetSpy.mockReturnValue({ on, off: jest.fn(), emit: emitSpy }); + }); + + it('constructs a ComposableContainer', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + expect(container).toBeInstanceOf(ComposableContainer); + }); + + // The controller-frame emit (frame-element-init) no longer passes a reply + // callback, so the ready listener must not assume `callback` is a function — + // otherwise it throws "callback is not a function" on init. + it('registerReadyListener: tolerates a controller emit with no reply callback', () => { + const onSpy = jest.spyOn(bus, 'on'); + // uuid is mocked to a constant, so all test containers share the event name; + // clear so mock.calls only holds this container's registrations. + onSpy.mockClear(); + const container = new ComposableContainer(metaData, context, { layout: [1] }); + const readyEvent = `COMPOSABLE_CONTAINER${(container as any).containerId}`; + const readyCall = onSpy.mock.calls.find(([event]) => event === readyEvent); + expect(readyCall).toBeDefined(); + const readyHandler = readyCall![1]; + // 3rd arg (reply callback) omitted by the emitter -> callback is undefined + expect(() => readyHandler({}, undefined)).not.toThrow(); + expect((container as any).isComposableFrameReady).toBe(true); + }); + + // The composable collect base keeps the default "Creating Collect container" log + // (only composable reveal overrides it). + it('getCreateContainerLog returns the collect message', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + expect((container as any).getCreateContainerLog()).toBe(logs.infoLogs.CREATE_COLLECT_CONTAINER); + }); + + it('create() returns a ComposableElement for a flowDB (tableName) input', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + const element = container.create(cvvElementInput); + expect(element).toBeInstanceOf(ComposableElement); + }); + + // flowDB has no file-element support; file types are rejected at create(). + it('create() rejects FILE_INPUT / MULTI_FILE_INPUT element types', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + expect(() => container.create({ + tableName: 'cards', column: 'file', type: FileElementType.FILE_INPUT, + } as any)).toThrow(SkyflowError); + expect(() => container.create({ + tableName: 'cards', column: 'files', type: FileElementType.MULTI_FILE_INPUT, + } as any)).toThrow(SkyflowError); + }); + + // The documented identity key is `tableName`; a client-supplied `table` is + // rejected so the composable path matches the collect path (previously `table` + // silently broke here because of the spread order). + it('create() rejects a client-supplied `table` key', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + expect(() => container.create({ + table: 'cards', column: 'cvv', type: BaseElementType.CVV, + } as any)).toThrow(SkyflowError); + }); + + it('collect() rejects a @core SkyflowError when no elements are added', (done) => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + container.collect().catch((err) => { + expect(err).toBeInstanceOf(SkyflowError); + expect(err.error.code).toBe(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.code); + done(); + }); + }); + + it('collect() rejects COMPOSABLE_CONTAINER_NOT_MOUNTED before mount', (done) => { + const container = new ComposableContainer(metaData, context, { + layout: [2], styles: { base: { width: '100px' } }, + }); + container.create(cvvElementInput); + container.create(cardNumberElement); + container.collect().catch((err) => { + expect(err.error.code).toBe(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED.code); + done(); + }); + }); + + it('collect() resolves the unified { records } response on success', async () => { + const div = document.createElement('div'); + div.id = 'composable'; + document.body.append(div); + const container = new ComposableContainer(metaData, context, { + layout: [2], styles: { base: { width: '100px' } }, + }); + container.create(cvvElementInput); + container.create(cardNumberElement); + container.mount('#composable'); + + const options: ICollectOptions = { tokens: true } as any; + const success = container.collect(options); + window.dispatchEvent(new MessageEvent('message', { + origin: properties.IFRAME_SECURE_ORIGIN, + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + '1234', + data: { ...collectResponse }, + }, + })); + await expect(success).resolves.toEqual(collectResponse); + }); + + it('collect() wraps a full API failure as SkyflowFlowDBError', async () => { + const div = document.createElement('div'); + div.id = 'composable'; + document.body.append(div); + const container = new ComposableContainer(metaData, context, { + layout: [2], styles: { base: { width: '100px' } }, + }); + container.create(cvvElementInput); + container.create(cardNumberElement); + container.mount('#composable'); + + const failure = container.collect({ tokens: true } as any); + window.dispatchEvent(new MessageEvent('message', { + origin: properties.IFRAME_SECURE_ORIGIN, + data: { + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + '1234', + data: { error: { http_code: 500, message: 'boom' } }, + }, + })); + await expect(failure).rejects.toBeInstanceOf(SkyflowFlowDBError); + }); + + it('unmount() delegates to the mounted container element', () => { + const div = document.createElement('div'); + div.id = 'composable'; + document.body.append(div); + const container = new ComposableContainer(metaData, context, { layout: [2] }); + container.create(cvvElementInput); + container.create(cardNumberElement); + container.mount('#composable'); + container.unmount(); + expect(mockUnmount).toBeCalled(); + }); + + // validateCollectOptions is the seam that previously delegated to @core's + // privacyDB-shaped validators. These assert the container now validates against + // the flowDB shapes (and forces tokens on) — proving the B1/B2 wiring, not just + // the standalone validators. + describe('validateCollectOptions (flowDB shapes)', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + const validate = (options: any) => (container as any).validateCollectOptions(options); + + it('B1: accepts a flowDB upsert ({ tableName, uniqueColumns }) and forces tokens on', () => { + const options = { upsert: [{ tableName: 'cards', uniqueColumns: ['card_number'] }] }; + expect(() => validate(options)).not.toThrow(); + expect(validate(options)).toEqual({ ...options, tokens: true }); + }); + + it('B2: accepts a flowDB additionalFields ({ tableName, data })', () => { + const options = { additionalFields: { records: [{ tableName: 'cards', data: { cvv: '123' } }] } }; + expect(() => validate(options)).not.toThrow(); + expect(validate(options).tokens).toBe(true); + }); + + it('rejects the privacyDB upsert shape ({ table, column })', () => { + expect(() => validate({ upsert: [{ table: 'cards', column: 'card_number' }] })) + .toThrow(SkyflowError); + }); + + it('rejects the privacyDB additionalFields shape ({ table, fields })', () => { + expect(() => validate({ additionalFields: { records: [{ table: 'cards', fields: { cvv: '1' } }] } })) + .toThrow(SkyflowError); + }); + }); + + // wrapCollectError maps a truthy error to SkyflowFlowDBError; a falsy error + // (the deferred/no-error path) passes through unchanged. + describe('wrapCollectError', () => { + const container = new ComposableContainer(metaData, context, { layout: [1] }); + const wrap = (err: any) => (container as any).wrapCollectError(err); + + it('wraps a truthy error as SkyflowFlowDBError', () => { + expect(wrap({ http_code: 500, message: 'boom' })).toBeInstanceOf(SkyflowFlowDBError); + }); + + it('passes a falsy error through unchanged', () => { + expect(wrap(null)).toBeNull(); + expect(wrap(undefined)).toBeUndefined(); + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/external/reveal/composable-reveal-container.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/external/reveal/composable-reveal-container.flowdb.test.ts new file mode 100644 index 000000000..27bb7db6c --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/reveal/composable-reveal-container.flowdb.test.ts @@ -0,0 +1,151 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB composable reveal container tests. The shared reveal() orchestration is +// covered by the @core suite; here we assert the flowDB divergence injected into +// the subclass: create() builds this package's ComposableRevealElement, +// instantiateInternalElement builds the token-only internal element, +// validateRecords/validateOptions run the flowDB validators, revealExtraData +// forwards the reveal options into the frame payload, and handleRevealResponse +// maps a full failure ({ error }) to SkyflowFlowDBError while resolving on success. +import SkyflowError from '@core/errors'; +import logs from '@core/utils/logs'; +import { LogLevel, Env, Context } from '../../../../src/utils/common'; +import ComposableRevealContainer from '../../../../src/external/reveal/composable-reveal-container'; +import ComposableRevealElement from '../../../../src/external/reveal/composable-reveal-element'; +import ComposableRevealInternalElement from '../../../../src/external/reveal/composable-reveal-internal'; +import SkyflowFlowDBError from '../../../../src/libs/skyflow-flowdb-error'; +import { ContainerType } from '../../../../src/skyflow'; +import { Metadata } from '../../../../src/internal/internal-types'; + +jest.mock('@core/iframe-libs/iframer', () => { + const actualModule = jest.requireActual('@core/iframe-libs/iframer'); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => 'https://google.com'); + return mockedModule; +}); + +const mockUuid = '1234'; +jest.mock('@core/libs/uuid', () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); + +const metaData: Metadata = { + uuid: '123', + sdkVersion: '', + sessionId: '1234', + clientDomain: 'http://abc.com', + containerType: ContainerType.COMPOSE_REVEAL, + clientJSON: { + config: { + vaultID: 'vault123', + vaultURL: 'https://sb.vault.dev', + getBearerToken, + }, + metaData: { + uuid: '123', + clientDomain: 'http://abc.com', + }, + }, + getSkyflowBearerToken: getBearerToken, + skyflowContainer: { + isControllerFrameReady: true, + } as any, +}; + +const context: Context = { logLevel: LogLevel.ERROR, env: Env.PROD }; +const options = { layout: [1] }; + +describe('flowDB composable reveal container', () => { + it('constructs a ComposableRevealContainer', () => { + const container = new ComposableRevealContainer(metaData, context, options); + expect(container).toBeInstanceOf(ComposableRevealContainer); + }); + + // The container-create log must identify a Reveal container, not "Creating Collect + // container" (the shared base default that composable reveal previously inherited). + it('getCreateContainerLog returns the reveal message, not the collect one', () => { + const container = new ComposableRevealContainer(metaData, context, options); + expect((container as any).getCreateContainerLog()).toBe(logs.infoLogs.CREATE_REVEAL_CONTAINER); + expect((container as any).getCreateContainerLog()) + .not.toBe(logs.infoLogs.CREATE_COLLECT_CONTAINER); + }); + + // create() delegates to the base buildComposableRevealElement and returns this + // package's ComposableRevealElement. + it('create() returns a ComposableRevealElement', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const element = container.create({ token: '1815-6223-1073-1425' }); + expect(element).toBeInstanceOf(ComposableRevealElement); + }); + + // instantiateInternalElement builds the token-only internal element. + it('instantiateInternalElement builds a ComposableRevealInternalElement', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const internal = (container as any).instantiateInternalElement('el-1', {}); + expect(internal).toBeInstanceOf(ComposableRevealInternalElement); + }); + + // validateRecords runs the flowDB token-only reveal-record validator. + describe('validateRecords (token-only)', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const validate = (records: any[]) => (container as any).validateRecords(records); + + it('accepts a valid token record', () => { + expect(() => validate([{ token: '1815-6223-1073-1425' }])).not.toThrow(); + }); + + it('throws when the token key is missing', () => { + expect(() => validate([{ label: 'x' }])).toThrow(SkyflowError); + }); + }); + + // validateOptions runs the flowDB tokenGroupRedactions validator. + describe('validateOptions (tokenGroupRedactions)', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const validate = (opts?: any) => (container as any).validateOptions(opts); + + it('is a no-op when options are absent', () => { + expect(() => validate(undefined)).not.toThrow(); + }); + + it('throws when tokenGroupRedactions is not an array', () => { + expect(() => validate({ tokenGroupRedactions: 'nope' })).toThrow(SkyflowError); + }); + }); + + // revealExtraData forwards the reveal options into the frame payload. + it('revealExtraData wraps the options under { options }', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const revealOptions = { tokenGroupRedactions: [{ tokenGroupName: 'g', redaction: 'MASKED' }] }; + expect((container as any).revealExtraData(revealOptions)).toEqual({ options: revealOptions }); + }); + + // handleRevealResponse: a full failure ({ error }) rejects with SkyflowFlowDBError; + // otherwise it resolves with the reveal data. + describe('handleRevealResponse', () => { + it('rejects a full failure ({ error }) as SkyflowFlowDBError', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const resolve = jest.fn(); + const reject = jest.fn(); + (container as any).handleRevealResponse({ error: { message: 'boom' } }, resolve, reject); + expect(resolve).not.toHaveBeenCalled(); + expect(reject).toHaveBeenCalledTimes(1); + expect(reject.mock.calls[0][0]).toBeInstanceOf(SkyflowFlowDBError); + }); + + it('resolves the reveal data on success', () => { + const container = new ComposableRevealContainer(metaData, context, options); + const resolve = jest.fn(); + const reject = jest.fn(); + const data = { success: [{ token: 't1' }] }; + (container as any).handleRevealResponse(data, resolve, reject); + expect(reject).not.toHaveBeenCalled(); + expect(resolve).toHaveBeenCalledWith(data); + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/external/reveal/reveal-container.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/external/reveal/reveal-container.flowdb.test.ts new file mode 100644 index 000000000..25a1b39bc --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/reveal/reveal-container.flowdb.test.ts @@ -0,0 +1,146 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB reveal container tests. The shared reveal()/mount() mechanics are covered +// by the @core suite; here we assert the flowDB divergence injected into the +// subclass: createRevealElement builds this package's RevealElement, validateRecords +// runs the token-only reveal-record validator, validateOptions runs the flowDB +// tokenGroupRedactions validator, and wrapRevealError maps to SkyflowFlowDBError. +import bus from 'framebus'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import SkyflowError from '@core/errors'; +import logs from '@core/utils/logs'; +import { LogLevel, Env, Context } from '../../../../src/utils/common'; +import RevealContainer from '../../../../src/external/reveal/reveal-container'; +import RevealElement from '../../../../src/external/reveal/reveal-element'; +import SkyflowFlowDBError from '../../../../src/libs/skyflow-flowdb-error'; +import { ContainerType } from '../../../../src/skyflow'; +import { Metadata } from '../../../../src/internal/internal-types'; + +jest.mock('@core/iframe-libs/iframer', () => { + const actualModule = jest.requireActual('@core/iframe-libs/iframer'); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => 'https://google.com'); + return mockedModule; +}); + +const mockUuid = '1234'; +jest.mock('@core/libs/uuid', () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); + +const metaData: Metadata = { + uuid: '123', + sdkVersion: '', + sessionId: '1234', + clientDomain: 'http://abc.com', + containerType: ContainerType.REVEAL, + clientJSON: { + config: { + vaultID: 'vault123', + vaultURL: 'https://sb.vault.dev', + getBearerToken, + }, + metaData: { + uuid: '123', + clientDomain: 'http://abc.com', + }, + }, + getSkyflowBearerToken: getBearerToken, + skyflowContainer: { + isControllerFrameReady: true, + } as any, +}; + +const context: Context = { logLevel: LogLevel.ERROR, env: Env.PROD }; + +describe('flowDB reveal container', () => { + const on = jest.fn(); + let targetSpy: jest.SpyInstance; + + beforeEach(() => { + jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + jest.spyOn(bus, 'on'); + targetSpy.mockReturnValue({ on, off: jest.fn(), emit: jest.fn() }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('constructs a RevealContainer', () => { + const container = new RevealContainer(metaData, context); + expect(container).toBeInstanceOf(RevealContainer); + expect(container).toHaveProperty('create'); + expect(container).toHaveProperty('reveal'); + }); + + // create() delegates to the injected createRevealElement factory, which builds + // this package's (token-only) RevealElement. + it('create() returns a flowDB RevealElement', () => { + const container = new RevealContainer(metaData, context); + const element = container.create({ token: '1815-6223-1073-1425' }); + expect(element).toBeInstanceOf(RevealElement); + }); + + // validateRecords runs the flowDB token-only reveal-record validator. + describe('validateRecords (token-only)', () => { + const container = new RevealContainer(metaData, context); + const validate = (records: any[]) => (container as any).validateRecords(records); + + it('accepts a valid token record', () => { + expect(() => validate([{ token: '1815-6223-1073-1425' }])).not.toThrow(); + }); + + it('throws on an empty records array', () => { + expect(() => validate([])).toThrow(SkyflowError); + }); + + it('throws when the token key is missing', () => { + expect(() => validate([{ label: 'x' }])).toThrow(SkyflowError); + }); + }); + + // validateOptions runs the flowDB tokenGroupRedactions validator. + describe('validateOptions (tokenGroupRedactions)', () => { + const container = new RevealContainer(metaData, context); + const validate = (options?: any) => (container as any).validateOptions(options); + + it('is a no-op when options are absent', () => { + expect(() => validate(undefined)).not.toThrow(); + }); + + it('accepts a valid tokenGroupRedactions array', () => { + expect(() => validate({ + tokenGroupRedactions: [{ tokenGroupName: 'g1', redaction: 'MASKED' }], + })).not.toThrow(); + }); + + it('throws when tokenGroupRedactions is not an array', () => { + expect(() => validate({ tokenGroupRedactions: 'nope' })).toThrow(SkyflowError); + }); + }); + + // wrapRevealError maps any reveal error onto SkyflowFlowDBError. + it('wrapRevealError maps the error to SkyflowFlowDBError', () => { + const container = new RevealContainer(metaData, context); + const wrapped = (container as any).wrapRevealError({ http_code: 404, message: 'Not Found' }); + expect(wrapped).toBeInstanceOf(SkyflowFlowDBError); + }); + + it('reveal() rejects when there are no reveal elements', (done) => { + const container = new RevealContainer(metaData, context); + container.reveal().catch((error: any) => { + expect(error).toBeInstanceOf(SkyflowError); + expect(error.error.code).toEqual(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_REVEAL.code); + expect(error.error.description).toEqual(logs.errorLogs.NO_ELEMENTS_IN_REVEAL); + done(); + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/internal/composable-frame-element-init.flowdb.test.js b/packages/skyflow-flowvault-js/tests/core/internal/composable-frame-element-init.flowdb.test.js new file mode 100644 index 000000000..375d8f294 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/internal/composable-frame-element-init.flowdb.test.js @@ -0,0 +1,162 @@ +/* +Copyright (c) 2022 Skyflow, Inc. + +flowDB equivalent of the reveal-response tests in composable-frame-element-init.test.js. +The composable reveal frame now uses the flowDB variant +(fetchRecordsByTokenIdComposableFlowDB / formatRecordsForClientComposableFlowDB), whose success +records carry token only (no valueType) and expose the index-0 shape { 0: {...}, frameId }. +*/ +import RevealComposableFrameElementInit from '../../../src/internal/composable-frame-element-init'; +import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_REVEAL, REVEAL_TYPES } from '@core/constants'; +import bus from 'framebus'; + +// Control the flowDB composable fetch per test; keep the real formatter so the posted shape is real. +const mockFetchRecordsByTokenIdComposableFlowDB = jest.fn(); +jest.mock('../../../src/api-utils/reveal', () => { + const actual = jest.requireActual('../../../src/api-utils/reveal'); + return { + ...actual, + fetchRecordsByTokenIdComposableFlowDB: (...args) => mockFetchRecordsByTokenIdComposableFlowDB(...args), + }; +}); + +const element = { + elementName: 'element:group:W29iamVjdCBPYmplY3Rd', + rows: [{ + elements: [{ + elementType: 'REVEAL', + elementName: 'reveal-composable:123', + name: 'reveal-composable:123', + table: 'patients', + column: 'card_number', + token: 'skyflow-id-1', + elementId: 'element-id-1', + }], + }], + clientDomain: 'http://localhost.com', +}; + +const on = jest.fn(); +const emit = jest.fn(); + +describe('composable flowDB reveal responses', () => { + let emitSpy; + let windowSpy; + let targetSpy; + + beforeEach(() => { + windowSpy = jest.spyOn(global, 'window', 'get'); + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, 'emit'); + targetSpy = jest.spyOn(bus, 'target'); + targetSpy.mockReturnValue({ on, emit }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('posts flowDB client shape (token only, no valueType) on success', async () => { + const containerId = 'reveal-success-flowdb'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + + // flowDB composable success record: index-0 shape + frameId, no value in client output + mockFetchRecordsByTokenIdComposableFlowDB.mockResolvedValue({ + records: [{ 0: { token: 'skyflow-id-1', value: '4111111111111111', httpCode: 200 }, frameId: 'reveal-composable:123' }], + }); + + let assertedType = false; + const postMessageSpy = jest.fn().mockImplementation((data) => { + if (data.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + containerId) { + assertedType = true; + expect(data.data).toEqual({ records: [{ token: 'skyflow-id-1', httpCode: 200 }] }); + expect(data.data.records[0].value).toBeUndefined(); + } + }); + let messageHandler; + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' } }, + containerId, + }))}`, + }, + parent: { postMessage: postMessageSpy }, + addEventListener: (event, handler) => { if (event === 'message') messageHandler = handler; }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + postMessageSpy.mockClear(); + + messageHandler({ + origin: 'http://localhost.com', + data: { + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context: { vaultId: 'vault-id-1' }, + data: { + elementIds: [{ frameId: 'reveal-composable:123' }], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig: { clientDomain: 'http://localhost.com', uuid: 'uuid-1', authToken: 'test-token' }, + }, + }); + + await new Promise((resolve) => { setTimeout(resolve, 100); }); + expect(assertedType).toBe(true); + }); + + test('posts flowDB error shape on reject', async () => { + const containerId = 'reveal-error-flowdb'; + const id = `${COMPOSABLE_REVEAL}:${containerId}:ERROR:`; + + mockFetchRecordsByTokenIdComposableFlowDB.mockRejectedValue({ + errors: [{ token: 'skyflow-id-1', error: { code: 404, description: 'Token not found' }, frameId: 'reveal-composable:123' }], + }); + + let assertedType = false; + const postMessageSpy = jest.fn().mockImplementation((data) => { + if (data.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + containerId) { + assertedType = true; + expect(data.data).toEqual({ + records: [{ error: 'Token not found', token: 'skyflow-id-1', httpCode: 404 }], + }); + } + }); + let messageHandler; + windowSpy.mockImplementation(() => ({ + name: id, + location: { + href: `http://localhost/?${btoa(JSON.stringify({ + record: element, + clientJSON: { metaData: { clientDomain: 'http://localhost.com' } }, + containerId, + }))}`, + }, + parent: { postMessage: postMessageSpy }, + addEventListener: (event, handler) => { if (event === 'message') messageHandler = handler; }, + })); + + RevealComposableFrameElementInit.startFrameElement(); + postMessageSpy.mockClear(); + + messageHandler({ + origin: 'http://localhost.com', + data: { + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + context: { vaultId: 'vault-id-1' }, + data: { + elementIds: [{ frameId: 'reveal-composable:123' }], + type: REVEAL_TYPES.REVEAL, + name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + containerId, + }, + clientConfig: { clientDomain: 'http://localhost.com', uuid: 'uuid-1', authToken: 'test-token' }, + }, + }); + + await new Promise((resolve) => { setTimeout(resolve, 100); }); + expect(assertedType).toBe(true); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/internal/frame-element-init.flowdb.test.js b/packages/skyflow-flowvault-js/tests/core/internal/frame-element-init.flowdb.test.js new file mode 100644 index 000000000..00a5f6997 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/internal/frame-element-init.flowdb.test.js @@ -0,0 +1,371 @@ +// flowDB variant of the frame-element-init tokenize orchestration tests. +// Mirrors the (currently skipped) privacyDB tokenize tests in +// frame-element-init.additional.test.js and frame-element-init.fulfilled-errors.test.js, +// but mocks the flowDB collect functions that frame-element-init now uses. +import { ELEMENTS } from '@core/constants'; + +// Mock collect helpers BEFORE importing FrameElementInit so internal references use mocks +jest.mock('../../../src/api-utils/collect', () => { + const constructElementsInsertReq = jest.fn((insertObj, updateObj) => [ + { records: Object.entries(insertObj).map(([table, fields]) => ({ table, fields })) }, + { updateRecords: Object.entries(updateObj).map(([skyflowID, record]) => ({ skyflowID, ...record })) }, + ]); + const constructFlowDBInsertRequest = jest.fn(() => ({ vaultID: 'vault123', records: [] })); + const constructFlowDBUpdateRequest = jest.fn(() => ({ vaultID: 'vault123', records: [] })); + const insertDataInCollectFlowDB = jest.fn(() => Promise.resolve({ records: [{ id: 'insert1' }], errors: [] })); + const updateDataInCollectFlowDB = jest.fn(() => Promise.resolve({ records: [{ id: 'update1' }], errors: [] })); + // Use the real implementation so CVV substitution behavior can be asserted. + const { replaceCVVTokensInResponse } = jest.requireActual('../../../src/api-utils/collect'); + return { + __esModule: true, + constructElementsInsertReq, + constructFlowDBInsertRequest, + constructFlowDBUpdateRequest, + insertDataInCollectFlowDB, + updateDataInCollectFlowDB, + replaceCVVTokensInResponse, + }; +}); +import FrameElementInit from '../../../src/internal/frame-element-init'; +import { + constructElementsInsertReq, + constructFlowDBInsertRequest, + constructFlowDBUpdateRequest, + insertDataInCollectFlowDB, + updateDataInCollectFlowDB, +} from '../../../src/api-utils/collect'; + +const nodeCrypto = require('crypto'); +Object.defineProperty(window, 'crypto', { + configurable: true, + value: { getRandomValues: (arr) => nodeCrypto.randomFillSync(arr) }, +}); + +const makeTextElement = ({ name = 'field1', tableName = 'patients', value = 'abc', isValid = true, isComplete = true, skyflowID } = {}) => ({ + state: { name, value, isValid, isComplete, isRequired: false }, + tableName, + validations: undefined, + doesClientHasError: false, + clientErrorText: '', + errorText: 'invalid', + fieldType: 'INPUT_FIELD', + skyflowID, + onFocusChange: jest.fn(), + setValue: jest.fn(), + getUnformattedValue: () => value, +}); + +const config = { vaultURL: 'https://vault.url', vaultID: 'vault123', authToken: 'token123' }; + +beforeEach(() => { + const payload = { record: { rows: [] }, metaData: { clientDomain: 'http://localhost.com', clientJSON: { config: { options: {} } } }, containerId: 'group' }; + const encoded = btoa(JSON.stringify(payload)); + jest.spyOn(global, 'window', 'get').mockReturnValue({ + name: 'FRAME_ELEMENT:group:123:ERROR:', + location: { href: `http://localhost/?${encoded}` }, + parent: { postMessage: jest.fn() }, + addEventListener: jest.fn(), + }); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('FrameElementInit tokenize (flowDB variant)', () => { + test('accumulates checkbox values into comma-separated string', async () => { + const instance = new FrameElementInit(); + const checkbox1 = { ...makeTextElement({ name: 'agree', value: 'yes' }), fieldType: ELEMENTS.checkbox.name }; + const checkbox2 = { ...makeTextElement({ name: 'agree', value: 'no' }), fieldType: ELEMENTS.checkbox.name }; + instance.iframeFormList = [checkbox1, checkbox2]; + await instance['tokenize']({ options: {} }, config); + const firstCallInsertObj = constructElementsInsertReq.mock.calls[0][0]; + expect(firstCallInsertObj.agree).toBe('yes,no'); + }); + + test('builds updateRecords for same skyflowID and resolves update-only', async () => { + const instance = new FrameElementInit(); + const e1 = makeTextElement({ name: 'first', tableName: 'patients', value: 'A', skyflowID: 'id123' }); + const e2 = makeTextElement({ name: 'second', tableName: 'patients', value: 'B', skyflowID: 'id123' }); + instance.iframeFormList = [e1, e2]; + // both elements carry the same skyflowID -> no inserts, one update + updateDataInCollectFlowDB.mockResolvedValue({ records: [{ id: 'upd1' }] }); + const result = await instance['tokenize']({ options: {} }, config); + expect(result.records[0].id).toBe('upd1'); + expect(insertDataInCollectFlowDB).not.toHaveBeenCalled(); + expect(updateDataInCollectFlowDB).toHaveBeenCalledTimes(1); + const updateObj = constructElementsInsertReq.mock.calls[0][1]; + expect(updateObj).toHaveProperty('id123'); + }); + + test('insert-only path resolves with insert records', async () => { + const instance = new FrameElementInit(); + const elem = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + instance.iframeFormList = [elem]; + constructElementsInsertReq.mockImplementation(() => [ { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, { updateRecords: [] } ]); + insertDataInCollectFlowDB.mockResolvedValue({ records: [{ id: 'ins1' }] }); + const res = await instance['tokenize']({ options: {} }, config); + expect(res.records[0].id).toBe('ins1'); + expect(updateDataInCollectFlowDB).not.toHaveBeenCalled(); + }); + + test('replaces the CVV element token with the fixed 3-digit mock (817) when returnMockValue is true, leaving sibling tokens intact', async () => { + const instance = new FrameElementInit(); + const cvv = { ...makeTextElement({ name: 'cvv', tableName: 'cards', value: '123' }), fieldType: ELEMENTS.CVV.name, returnMockValue: true }; + const cardNumber = makeTextElement({ name: 'card_number', tableName: 'cards', value: '4111111111111111' }); + instance.iframeFormList = [cvv, cardNumber]; + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'cards', fields: { cvv: '123', card_number: '4111111111111111' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ + records: [{ + tableName: 'cards', + tokens: { + cvv: [{ token: 'real-cvv-token', tokenGroupName: 'det' }], + card_number: [{ token: 'real-card-token', tokenGroupName: 'det' }], + }, + httpCode: 200, + }], + }); + const res = await instance['tokenize']({ options: {} }, config); + const cvvToken = res.records[0].tokens.cvv[0].token; + expect(cvvToken).toHaveLength(3); + expect(cvvToken).toEqual('817'); + expect(cvvToken).not.toEqual('123'); + expect(cvvToken).not.toEqual('real-cvv-token'); + expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); + }); + + test('replaces the CVV element token with the fixed 4-digit mock (8173) when returnMockValue is true, leaving sibling tokens intact', async () => { + const instance = new FrameElementInit(); + const cvv = { ...makeTextElement({ name: 'cvv', tableName: 'cards', value: '1234' }), fieldType: ELEMENTS.CVV.name, returnMockValue: true }; + const cardNumber = makeTextElement({ name: 'card_number', tableName: 'cards', value: '4111111111111111' }); + instance.iframeFormList = [cvv, cardNumber]; + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'cards', fields: { cvv: '1234', card_number: '4111111111111111' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ + records: [{ + tableName: 'cards', + tokens: { + cvv: [{ token: 'real-cvv-token', tokenGroupName: 'det' }], + card_number: [{ token: 'real-card-token', tokenGroupName: 'det' }], + }, + httpCode: 200, + }], + }); + const res = await instance['tokenize']({ options: {} }, config); + const cvvToken = res.records[0].tokens.cvv[0].token; + expect(cvvToken).toHaveLength(4); + expect(cvvToken).toEqual('8173'); + expect(cvvToken).not.toEqual('1234'); + expect(cvvToken).not.toEqual('real-cvv-token'); + expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); + }); + + test('does not replace the CVV element token when returnMockValue is false', async () => { + const instance = new FrameElementInit(); + const cvv = { ...makeTextElement({ name: 'cvv', tableName: 'cards', value: '1234' }), fieldType: ELEMENTS.CVV.name, returnMockValue: false }; + const cardNumber = makeTextElement({ name: 'card_number', tableName: 'cards', value: '4111111111111111' }); + instance.iframeFormList = [cvv, cardNumber]; + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'cards', fields: { cvv: '1234', card_number: '4111111111111111' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ + records: [{ + tableName: 'cards', + tokens: { + cvv: [{ token: 'real-cvv-token', tokenGroupName: 'det' }], + card_number: [{ token: 'real-card-token', tokenGroupName: 'det' }], + }, + httpCode: 200, + }], + }); + const res = await instance['tokenize']({ options: {} }, config); + const cvvToken = res.records[0].tokens.cvv[0].token; + expect(cvvToken).toEqual('real-cvv-token'); + expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); + }); + + test('does not replace the CVV element token when returnMockValue is omitted (defaults to no mock)', async () => { + const instance = new FrameElementInit(); + const cvv = { ...makeTextElement({ name: 'cvv', tableName: 'cards', value: '1234' }), fieldType: ELEMENTS.CVV.name }; + const cardNumber = makeTextElement({ name: 'card_number', tableName: 'cards', value: '4111111111111111' }); + instance.iframeFormList = [cvv, cardNumber]; + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'cards', fields: { cvv: '1234', card_number: '4111111111111111' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ + records: [{ + tableName: 'cards', + tokens: { + cvv: [{ token: 'real-cvv-token', tokenGroupName: 'det' }], + card_number: [{ token: 'real-card-token', tokenGroupName: 'det' }], + }, + httpCode: 200, + }], + }); + const res = await instance['tokenize']({ options: {} }, config); + expect(res.records[0].tokens.cvv[0].token).toEqual('real-cvv-token'); + expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); + }); + + test('does not mock a non-CVV element even when returnMockValue is true (no-op)', async () => { + const instance = new FrameElementInit(); + const cardNumber = { ...makeTextElement({ name: 'card_number', tableName: 'cards', value: '4111111111111111' }), returnMockValue: true }; + instance.iframeFormList = [cardNumber]; + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'cards', fields: { card_number: '4111111111111111' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ + records: [{ + tableName: 'cards', + tokens: { + card_number: [{ token: 'real-card-token', tokenGroupName: 'det' }], + }, + httpCode: 200, + }], + }); + const res = await instance['tokenize']({ options: {} }, config); + expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); + }); + // SKIPPED (flowDB): assert V1/privacyDB aggregated {records,errors} reject contract; flowDB inlines per-record errors within records / uses {error} for full failure. TODO: re-enable/rewrite for flowDB. + test.skip('mixed insert/update with update errors returns combined object', async () => { + const instance = new FrameElementInit(); + const ins = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + const upd = makeTextElement({ name: 'first', tableName: 'patients', value: 'X', skyflowID: 'id999' }); + instance.iframeFormList = [ins, upd]; + constructElementsInsertReq.mockImplementation(() => [ { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, { updateRecords: [{ skyflowID: 'id999', table: 'patients', first: 'X' }] } ]); + insertDataInCollectFlowDB.mockResolvedValue({ records: [{ id: 'ins1' }] }); + updateDataInCollectFlowDB.mockResolvedValue({ errors: [{ code: 'E1' }] }); + await expect(instance['tokenize']({ options: {} }, config)).rejects.toEqual({ records: [{ id: 'ins1' }], errors: [{ code: 'E1' }] }); + }); + + // SKIPPED (flowDB): assert V1/privacyDB aggregated {records,errors} reject contract; flowDB inlines per-record errors within records / uses {error} for full failure. TODO: re-enable/rewrite for flowDB. + test.skip('error-only path rejects with aggregated errors (no records)', async () => { + const instance = new FrameElementInit(); + const ins = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + const upd = makeTextElement({ name: 'beta', tableName: 'patients', value: 'B', skyflowID: 'idErr' }); + instance.iframeFormList = [ins, upd]; + constructElementsInsertReq.mockImplementation(() => [ { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, { updateRecords: [{ skyflowID: 'idErr', table: 'patients', beta: 'B' }] } ]); + insertDataInCollectFlowDB.mockResolvedValue({ errors: [{ code: 'E_INS' }] }); + updateDataInCollectFlowDB.mockResolvedValue({ errors: [{ code: 'E_UPD' }] }); + await expect(instance['tokenize']({ options: {} }, config)).rejects.toEqual({ errors: [{ code: 'E_INS' }, { code: 'E_UPD' }] }); + }); + + // SKIPPED (flowDB): assert V1/privacyDB aggregated {records,errors} reject contract; flowDB inlines per-record errors within records / uses {error} for full failure. TODO: re-enable/rewrite for flowDB. + test.skip('error-only insert-only path rejects with aggregated errors', async () => { + const instance = new FrameElementInit(); + const ins = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + instance.iframeFormList = [ins]; + constructElementsInsertReq.mockImplementation(() => [ { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, { updateRecords: [] } ]); + insertDataInCollectFlowDB.mockResolvedValue({ errors: [{ code: 'E_INS' }] }); + await expect(instance['tokenize']({ options: {} }, config)).rejects.toEqual({ errors: [{ code: 'E_INS' }] }); + }); + + test('resolves with records when no errors', async () => { + const instance = new FrameElementInit(); + const ins = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + instance.iframeFormList = [ins]; + constructElementsInsertReq.mockImplementation(() => [ { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, { updateRecords: [] } ]); + insertDataInCollectFlowDB.mockResolvedValue({ records: [{ id: 'ins1' }] }); + await expect(instance['tokenize']({ options: {} }, config)).resolves.toEqual({ records: [{ id: 'ins1' }] }); + }); + + // SKIPPED (flowDB): assert V1/privacyDB aggregated {records,errors} reject contract; flowDB inlines per-record errors within records / uses {error} for full failure. TODO: re-enable/rewrite for flowDB. + test.skip('partial error: insert mixed records+errors, update errors', async () => { + const instance = new FrameElementInit(); + const ins = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + const upd = makeTextElement({ name: 'beta', tableName: 'patients', value: 'B', skyflowID: 'idErr' }); + instance.iframeFormList = [ins, upd]; + constructElementsInsertReq.mockImplementation(() => [ { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, { updateRecords: [{ skyflowID: 'idErr', table: 'patients', beta: 'B' }] } ]); + insertDataInCollectFlowDB.mockResolvedValue({ errors: [{ code: 'E_INS' }], records: [{ id: 'ins1' }] }); + updateDataInCollectFlowDB.mockResolvedValue({ errors: [{ code: 'E_UPD' }] }); + await expect(instance['tokenize']({ options: {} }, config)).rejects.toEqual({ errors: [{ code: 'E_INS' }, { code: 'E_UPD' }], records: [{ id: 'ins1' }] }); + }); + + test('catch path when request building throws', async () => { + const instance = new FrameElementInit(); + const elem = makeTextElement({ name: 'alpha', tableName: 'patients', value: 'A' }); + instance.iframeFormList = [elem]; + constructFlowDBInsertRequest.mockImplementation(() => { throw new Error('bad-request'); }); + await expect(instance['tokenize']({ options: {} }, config)).rejects.toEqual({ error: 'bad-request' }); + }); +}); + +describe('FrameElementInit static + dispatchCollectRequest branches (flowDB)', () => { + test('startFrameElement instantiates the singleton frame element', () => { + expect(() => FrameElementInit.startFrameElement()).not.toThrow(); + }); + + // dispatchCollectRequest is exercised directly to reach the errorMessages + // (setErrorMessages) branch and the per-response failure-reject branch. + test('applies client error messages and rejects the first failing response', async () => { + const instance = new FrameElementInit(); + // Reset the request builders (an earlier test left them throwing). + constructFlowDBInsertRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructFlowDBUpdateRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ error: { http_code: 500, message: 'boom' } }); + const errorMessages = { NOT_FOUND: 'custom not found' }; + await expect( + instance['dispatchCollectRequest']( + { patients: { alpha: 'A' } }, {}, {}, { options: {} }, config, errorMessages, + ), + ).rejects.toEqual({ error: { http_code: 500, message: 'boom' } }); + }); + + test('resolves { records } (no errorMessages) when every response succeeds', async () => { + const instance = new FrameElementInit(); + constructFlowDBInsertRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructFlowDBUpdateRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, + { updateRecords: [] }, + ]); + insertDataInCollectFlowDB.mockResolvedValue({ records: [{ id: 'ins1' }] }); + await expect( + instance['dispatchCollectRequest']( + { patients: { alpha: 'A' } }, {}, {}, { options: {} }, config, + ), + ).resolves.toEqual({ records: [{ id: 'ins1' }] }); + }); + + test('defaults to [] for a success response with no records key', async () => { + const instance = new FrameElementInit(); + constructFlowDBInsertRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructFlowDBUpdateRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructElementsInsertReq.mockImplementation(() => [ + { records: [{ table: 'patients', fields: { alpha: 'A' } }] }, + { updateRecords: [] }, + ]); + // A successful response that omits `records` exercises the `|| []` fallback. + insertDataInCollectFlowDB.mockResolvedValue({}); + await expect( + instance['dispatchCollectRequest']( + { patients: { alpha: 'A' } }, {}, {}, { options: {} }, config, + ), + ).resolves.toEqual({ records: [] }); + }); + + test('makes no request (pending promise) when there is nothing to insert or update', () => { + const instance = new FrameElementInit(); + constructFlowDBInsertRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructFlowDBUpdateRequest.mockImplementation(() => ({ vaultID: 'vault123', records: [] })); + constructElementsInsertReq.mockImplementation(() => [{ records: [] }, { updateRecords: [] }]); + const result = instance['dispatchCollectRequest']( + {}, {}, {}, { options: {} }, config, + ); + expect(result).toBeInstanceOf(Promise); + expect(insertDataInCollectFlowDB).not.toHaveBeenCalled(); + expect(updateDataInCollectFlowDB).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/core/internal/skyflow-frame-controller.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/internal/skyflow-frame-controller.flowdb.test.ts new file mode 100644 index 000000000..7fa7cd6c3 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/internal/skyflow-frame-controller.flowdb.test.ts @@ -0,0 +1,202 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB skyflow-frame controller tests. The shared bus topology + tokenize/ +// revealData skeleton live in @core (CoreSkyflowFrameController) and are covered by +// the skyflow-js suite; here we assert only the flowDB API-call divergence injected +// into the subclass: init(), the telemetry identity (getSdkNameAndVersion), the +// error-envelope shape (wrapCallbackError, both branches), the reveal fetch/format +// delegation, and the flowDB collect send path (sendCollectRequest, every branch). +import bus from 'framebus'; + +// Mock the flowDB api-utils BEFORE importing the controller so its internal +// references bind to the mocks. Reveal is delegated straight through; collect is +// driven branch-by-branch via sendCollectRequest. +jest.mock('../../../src/api-utils/collect', () => ({ + __esModule: true, + constructElementsInsertReq: jest.fn(() => [{ records: [] }, { updateRecords: [] }]), + constructFlowDBInsertRequest: jest.fn(() => ({ vaultID: 'vault123', records: [] })), + constructFlowDBUpdateRequest: jest.fn(() => ({ vaultID: 'vault123', updateRecords: [] })), + insertDataInCollectFlowDB: jest.fn(() => Promise.resolve({ records: [{ id: 'ins1' }] })), + updateDataInCollectFlowDB: jest.fn(() => Promise.resolve({ records: [{ id: 'upd1' }] })), + mergeFlowDBCollectResponses: jest.fn(() => ({ records: [{ id: 'merged' }] })), +})); + +jest.mock('../../../src/api-utils/reveal', () => ({ + __esModule: true, + fetchRecordsByTokenIdFlowDB: jest.fn(() => Promise.resolve({ records: [{ token: 't1' }] })), + formatRecordsForClientFlowDB: jest.fn((result) => ({ formatted: true, ...result })), +})); + +jest.mock('@core/utils/bus-events', () => ({ + ...jest.requireActual('@core/utils/bus-events'), + getAccessToken: jest.fn(() => Promise.resolve('access-token')), +})); + +import * as busEvents from '@core/utils/bus-events'; +import SkyflowFrameController from '../../../src/internal/skyflow-frame/skyflow-frame-controller'; +import { + constructElementsInsertReq, + constructFlowDBInsertRequest, + mergeFlowDBCollectResponses, +} from '../../../src/api-utils/collect'; +import { + fetchRecordsByTokenIdFlowDB, + formatRecordsForClientFlowDB, +} from '../../../src/api-utils/reveal'; + +const nodeCrypto = require('crypto'); +Object.defineProperty(window, 'crypto', { + configurable: true, + value: { getRandomValues: (arr: any) => nodeCrypto.randomFillSync(arr) }, +}); + +const flowDBClient = { + config: { vaultID: 'vault123', vaultURL: 'https://vault.test.com' }, + toJSON: () => ({ metaData: { uuid: 'client-uuid' } }), +}; + +// init() drives the base constructor, which wires bus listeners + emits the +// readiness handshake. We stub bus.target so those emits are inert, then set the +// resolved client/context directly to exercise the hooks in isolation. +const makeController = (): any => { + const controller: any = SkyflowFrameController.init('client-1'); + controller.client = flowDBClient; + controller.context = { logLevel: 4 }; + return controller; +}; + +describe('flowDB SkyflowFrameController', () => { + beforeEach(() => { + window.name = 'controller:frameId:Y2xpZW50RG9tYWlu:true'; + jest.spyOn(bus, 'target').mockReturnValue({ + on: jest.fn(), + emit: jest.fn(), + } as any); + jest.spyOn(bus, 'on').mockReturnValue(bus as any); + (busEvents.getAccessToken as jest.Mock).mockImplementation(() => Promise.resolve('access-token')); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init() returns a SkyflowFrameController instance', () => { + const controller = SkyflowFrameController.init('client-1'); + expect(controller).toBeInstanceOf(SkyflowFrameController); + }); + + it('init() defaults the clientId when none is supplied', () => { + const controller = SkyflowFrameController.init(); + expect(controller).toBeInstanceOf(SkyflowFrameController); + }); + + it('exposes the flowDB collect/reveal flags', () => { + const controller = makeController(); + expect(controller.collectsCVV).toBe(true); + expect(controller.revealResolvesPartialFailure).toBe(true); + }); + + it('getSdkNameAndVersion delegates to the flowDB telemetry helper', () => { + const controller = makeController(); + const info = controller.getSdkNameAndVersion('skyflow-flowvault-js@1.0.0'); + expect(info).toHaveProperty('sdkName'); + expect(info).toHaveProperty('sdkVersion'); + }); + + // wrapCallbackError: an already-enveloped body ({ error }) is forwarded as-is; + // anything else is wrapped under { error }. + describe('wrapCallbackError', () => { + it('forwards an already-enveloped error as-is', () => { + const controller = makeController(); + const enveloped = { error: { code: 400, message: 'boom' } }; + expect(controller.wrapCallbackError(enveloped)).toBe(enveloped); + }); + + it('wraps a bare error under { error }', () => { + const controller = makeController(); + const bare = 'plain-message'; + expect(controller.wrapCallbackError(bare)).toEqual({ error: 'plain-message' }); + }); + }); + + it('fetchRevealRecords delegates to fetchRecordsByTokenIdFlowDB', async () => { + const controller = makeController(); + const records = [{ token: 't1' }]; + const options = { tokenGroupRedactions: [] }; + const result = await controller.fetchRevealRecords(records, options); + expect(fetchRecordsByTokenIdFlowDB).toHaveBeenCalledWith(records, controller.client, options); + expect(result).toEqual({ records: [{ token: 't1' }] }); + }); + + it('formatRevealForClient delegates to formatRecordsForClientFlowDB', () => { + const controller = makeController(); + const raw = { records: [{ token: 't1' }] }; + const formatted = controller.formatRevealForClient(raw); + expect(formatRecordsForClientFlowDB).toHaveBeenCalledWith(raw); + expect(formatted).toEqual({ formatted: true, records: [{ token: 't1' }] }); + }); + + // sendCollectRequest: build the /v2 insert + update requests, fire them together, + // merge and re-map CVV tokens. Exercised branch-by-branch via a synthetic `built`. + describe('sendCollectRequest', () => { + const built = { insertResponseObject: {}, updateResponseObject: {}, cvvMap: {} } as any; + + it('fires insert + update, merges, and resolves with the merged records', async () => { + const controller = makeController(); + (constructElementsInsertReq as jest.Mock).mockReturnValueOnce([ + { records: [{ table: 'cards' }] }, { updateRecords: [{ skyflowID: 'id1' }] }, + ]); + (mergeFlowDBCollectResponses as jest.Mock).mockReturnValueOnce({ records: [{ id: 'ok' }] }); + await expect(controller.sendCollectRequest(built, {})).resolves.toEqual({ records: [{ id: 'ok' }] }); + }); + + it('resolves { records: [] } when there is nothing to insert or update', async () => { + const controller = makeController(); + (constructElementsInsertReq as jest.Mock).mockReturnValueOnce([ + { records: [] }, { updateRecords: [] }, + ]); + await expect(controller.sendCollectRequest(built, {})).resolves.toEqual({ records: [] }); + }); + + it('rejects the merged body when it carries no records (total failure)', async () => { + const controller = makeController(); + (constructElementsInsertReq as jest.Mock).mockReturnValueOnce([ + { records: [{ table: 'cards' }] }, { updateRecords: [] }, + ]); + (mergeFlowDBCollectResponses as jest.Mock).mockReturnValueOnce({ error: { message: 'all failed' } }); + await expect(controller.sendCollectRequest(built, {})).rejects.toEqual({ error: { message: 'all failed' } }); + }); + + it('rejects { error } when request building throws', async () => { + const controller = makeController(); + (constructFlowDBInsertRequest as jest.Mock).mockImplementationOnce(() => { + throw new Error('bad-request'); + }); + (constructElementsInsertReq as jest.Mock).mockReturnValueOnce([ + { records: [{ table: 'cards' }] }, { updateRecords: [] }, + ]); + await expect(controller.sendCollectRequest(built, {})).rejects.toEqual({ error: 'bad-request' }); + }); + + it('defaults the clientId to empty when the client metadata has no uuid', async () => { + const controller = makeController(); + controller.client = { config: { vaultID: 'vault123' }, toJSON: () => ({ metaData: {} }) }; + (constructElementsInsertReq as jest.Mock).mockReturnValueOnce([ + { records: [{ table: 'cards' }] }, { updateRecords: [] }, + ]); + (mergeFlowDBCollectResponses as jest.Mock).mockReturnValueOnce({ records: [{ id: 'ok' }] }); + await expect(controller.sendCollectRequest(built, {})).resolves.toEqual({ records: [{ id: 'ok' }] }); + }); + + it('rejects when the access-token fetch fails', async () => { + const controller = makeController(); + (constructElementsInsertReq as jest.Mock).mockReturnValueOnce([ + { records: [{ table: 'cards' }] }, { updateRecords: [] }, + ]); + (busEvents.getAccessToken as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error('token-fail'))); + await expect(controller.sendCollectRequest(built, {})).rejects.toThrow('token-fail'); + }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/jest.setup.js b/packages/skyflow-flowvault-js/tests/jest.setup.js new file mode 100644 index 000000000..42056ee55 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/jest.setup.js @@ -0,0 +1,10 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Mirror the webpack DefinePlugin SDK identity injection for the test runtime. +// jest does not run webpack, so define SDK_NAME/SDK_VERSION as globals from +// this package's own package.json (keeps telemetry output identical in tests). +const pkg = require('../package.json'); + +global.SDK_NAME = pkg.name; +global.SDK_VERSION = pkg.version; diff --git a/packages/skyflow-flowvault-js/tests/libs/skyflow-flowdb-error.test.js b/packages/skyflow-flowvault-js/tests/libs/skyflow-flowdb-error.test.js new file mode 100644 index 000000000..36f20dd46 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/libs/skyflow-flowdb-error.test.js @@ -0,0 +1,93 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import SkyflowError from '@core/errors'; +import SkyflowFlowDBError, { normalizeFlowDBError } from '../../src/libs/skyflow-flowdb-error'; + +describe('normalizeFlowDBError', () => { + it('maps snake_case flowDB keys to camelCase', () => { + expect( + normalizeFlowDBError({ + grpc_code: 5, + http_code: 404, + http_status: 'NOT_FOUND', + message: 'not found', + details: [{ a: 1 }], + }), + ).toEqual({ + grpcCode: 5, + httpCode: 404, + httpStatus: 'NOT_FOUND', + message: 'not found', + details: [{ a: 1 }], + }); + }); + + it('accepts camelCase input unchanged', () => { + expect( + normalizeFlowDBError({ + grpcCode: 3, httpCode: 400, httpStatus: 'BAD', message: 'bad', + }), + ).toEqual({ + grpcCode: 3, httpCode: 400, httpStatus: 'BAD', message: 'bad', + }); + }); + + it('emits only the keys that are present (minimal shape)', () => { + expect(normalizeFlowDBError({ http_code: 500 })).toEqual({ httpCode: 500 }); + expect(normalizeFlowDBError({})).toEqual({}); + expect(normalizeFlowDBError()).toEqual({}); + }); + + it('accepts a bare message string', () => { + expect(normalizeFlowDBError('boom')).toEqual({ message: 'boom' }); + }); + + it('treats a null raw body as empty (falls back to {} for destructuring)', () => { + expect(normalizeFlowDBError(null)).toEqual({}); + }); + + it('emits details only when present', () => { + expect(normalizeFlowDBError({ details: [{ x: 1 }] })).toEqual({ details: [{ x: 1 }] }); + expect(normalizeFlowDBError({ message: 'm' })).not.toHaveProperty('details'); + }); + + it('accepts the internal SkyflowError code/description shape', () => { + expect(normalizeFlowDBError({ code: 409, description: 'conflict' })) + .toEqual({ httpCode: 409, message: 'conflict' }); + }); +}); + +describe('SkyflowFlowDBError', () => { + it('extends the @core SkyflowError base and surfaces flowDB fields', () => { + const err = new SkyflowFlowDBError({ + grpc_code: 5, + http_code: 404, + http_status: 'NOT_FOUND', + message: 'nope', + details: [{ x: 1 }], + }); + expect(err).toBeInstanceOf(SkyflowError); + expect(err).toBeInstanceOf(Error); + expect(err.name).toBe('SkyflowError'); + expect(err.message).toBe('nope'); + expect(err.grpcCode).toBe(5); + expect(err.httpCode).toBe(404); + expect(err.httpStatus).toBe('NOT_FOUND'); + expect(err.details).toEqual([{ x: 1 }]); + expect(err.error).toMatchObject({ + grpcCode: 5, + httpCode: 404, + message: 'nope', + code: 404, + description: 'nope', + }); + }); + + it('handles an empty error body', () => { + const err = new SkyflowFlowDBError(); + expect(err).toBeInstanceOf(SkyflowError); + expect(err.name).toBe('SkyflowError'); + expect(err.error).toEqual({ code: '', description: '' }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/re-exports.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/re-exports.flowdb.test.ts new file mode 100644 index 000000000..d7fb287e0 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/re-exports.flowdb.test.ts @@ -0,0 +1,75 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// flowvault re-export / thin-subclass surface. These modules either re-export a +// variant-neutral @core default verbatim (so the package's own import paths stay +// stable) or bind a @core base class to flowDB's token-only reveal-input shape. +// Importing each executes its declaration; the assertions pin the identity / +// prototype chain that the split relies on. +import ComposableCollectElement from '../src/external/collect/compose-collect-element'; +import CoreComposableCollectElement from '@core/external/collect/composable-collect-element'; +import SkyflowContainer from '../src/external/skyflow-container'; +import CoreSkyflowContainer from '@core/external/skyflow-container'; +import RevealFrame from '../src/internal/reveal/reveal-frame'; +import CoreRevealFrame from '@core/internal/reveal/reveal-frame'; +import RevealElement from '../src/external/reveal/reveal-element'; +import CoreRevealElement from '@core/external/reveal/reveal-element'; +import ComposableRevealElement from '../src/external/reveal/composable-reveal-element'; +import CoreComposableRevealElement from '@core/external/reveal/composable-reveal-element'; +import ComposableRevealInternalElement from '../src/external/reveal/composable-reveal-internal'; +import CoreComposableRevealInternalElement from '@core/external/reveal/composable-reveal-internal'; +import * as coreLogsHelper from '@core/utils/logs-helper'; +import { + printLog, parameterizedString, getElementName, LogLevelOptions, EnvOptions, +} from '../src/utils/logs-helper'; +import { UpdateType } from '../src/utils/common'; + +describe('pure @core re-exports (identity preserved)', () => { + test('compose-collect-element re-exports the @core default', () => { + expect(ComposableCollectElement).toBe(CoreComposableCollectElement); + }); + + test('skyflow-container re-exports the @core default', () => { + expect(SkyflowContainer).toBe(CoreSkyflowContainer); + }); + + test('reveal-frame re-exports the @core default', () => { + expect(RevealFrame).toBe(CoreRevealFrame); + }); +}); + +describe('logs-helper re-exports the variant-neutral @core helpers', () => { + test('binds printLog / parameterizedString / getElementName from @core', () => { + expect(printLog).toBe(coreLogsHelper.printLog); + expect(parameterizedString).toBe(coreLogsHelper.parameterizedString); + expect(getElementName).toBe(coreLogsHelper.getElementName); + }); + + test('binds the LogLevelOptions / EnvOptions maps from @core', () => { + expect(LogLevelOptions).toBe(coreLogsHelper.LogLevelOptions); + expect(EnvOptions).toBe(coreLogsHelper.EnvOptions); + }); +}); + +describe('reveal element subclasses bind the @core bases', () => { + test('RevealElement extends the @core reveal element', () => { + expect(Object.getPrototypeOf(RevealElement)).toBe(CoreRevealElement); + }); + + test('ComposableRevealElement extends the @core composable reveal element', () => { + expect(Object.getPrototypeOf(ComposableRevealElement)).toBe(CoreComposableRevealElement); + }); + + test('ComposableRevealInternalElement extends the @core composable reveal-internal element', () => { + expect(Object.getPrototypeOf(ComposableRevealInternalElement)) + .toBe(CoreComposableRevealInternalElement); + }); +}); + +describe('UpdateType enum (flowDB-specific)', () => { + test('exposes exactly UPDATE and REPLACE', () => { + expect(UpdateType.UPDATE).toBe('UPDATE'); + expect(UpdateType.REPLACE).toBe('REPLACE'); + expect(Object.values(UpdateType)).toEqual(['UPDATE', 'REPLACE']); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js b/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js new file mode 100644 index 000000000..bee2dad07 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js @@ -0,0 +1,74 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Covers the flowDB `Skyflow` shell now that the constructor / init() / +// container() logic lives in the shared `@core/external/base-skyflow`. The +// important assertions are the seam ones: that the base constructor dispatches +// to this package's `instantiateSkyflowContainer` hook, that `container()` +// resolves each of the four flowDB container classes through the base switch, +// and that flowDB inherits the shared statics without picking up privacyDB's +// pure-JS surface (`ThreeDS`). +import * as iframerUtils from '@core/iframe-libs/iframer'; +import Skyflow, { ContainerType } from '../src/skyflow'; + +jest.spyOn(iframerUtils, 'getIframeSrc').mockImplementation(() => 'https://google.com'); + +const nodeCrypto = require('crypto'); +Object.defineProperty(window, 'crypto', { + configurable: true, + value: { getRandomValues: (arr) => nodeCrypto.randomFillSync(arr) }, +}); + +const config = { + vaultID: 'vault_id', + vaultURL: 'https://vault.test.com/', + getBearerToken: jest.fn(() => Promise.resolve('token')), +}; + +describe('flowDB Skyflow (BaseSkyflow subclass wiring)', () => { + test('init() returns a flowDB Skyflow and normalizes the vault URL', () => { + const s = Skyflow.init({ ...config }); + expect(s.constructor === Skyflow).toBe(true); + expect(s instanceof Skyflow).toBe(true); + }); + + test('base constructor dispatches to the subclass instantiateSkyflowContainer hook', () => { + const spy = jest.spyOn(Skyflow.prototype, 'instantiateSkyflowContainer'); + Skyflow.init({ ...config }); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.results[0].value).toBeDefined(); + spy.mockRestore(); + }); + + test('container() builds each of the four flowDB container types', () => { + const s = Skyflow.init({ ...config }); + expect(s.container(ContainerType.COLLECT).constructor.name).toBe('CollectContainer'); + expect(s.container(ContainerType.REVEAL).constructor.name).toBe('RevealContainer'); + expect(s.container(ContainerType.COMPOSABLE, { layout: [1] }).constructor.name).toBe('ComposableContainer'); + expect(s.container(ContainerType.COMPOSE_REVEAL, { layout: [1] }).constructor.name).toBe('ComposableRevealContainer'); + }); + + test('container() rejects a missing/invalid type', () => { + const s = Skyflow.init({ ...config }); + expect(() => s.container()).toThrow(); + expect(() => s.container('NOPE')).toThrow(); + }); + + test('inherited statics and the flowDB-specific ones are exposed', () => { + expect(Skyflow.ContainerType).toBeDefined(); + expect(Skyflow.ElementType).toBeDefined(); + expect(Skyflow.RedactionType).toBeDefined(); + expect(Skyflow.ErrorType).toBeDefined(); + expect(Skyflow.LogLevel).toBeDefined(); + expect(Skyflow.EventName).toBeDefined(); + expect(Skyflow.Env).toBeDefined(); + expect(Skyflow.ValidationRuleType).toBeDefined(); + expect(Skyflow.CardType).toBeDefined(); + expect(Skyflow.UpdateType).toBeDefined(); + expect(Skyflow.Error.name).toBe('SkyflowFlowDBError'); + expect(Skyflow.ThreeDS).toBeUndefined(); + // flowDB is elements-only (no invokeConnection / invokeGateway), so it must + // NOT inherit RequestMethod. See audit finding F1. + expect(Skyflow.RequestMethod).toBeUndefined(); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts new file mode 100644 index 000000000..487da32d2 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts @@ -0,0 +1,59 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB-only mock-CVV helper. `generateMockCVV` returns a FIXED mock keyed by +// CVV length (817 for 3 digits, 8173 for 4 digits) and '' for any other length. +// The value is constant, so it may coincide with a real CVV of 817/8173 — that is +// acceptable for the GA mock behaviour. +import { + generateMockCVV, + MOCK_CVV_THREE_DIGIT, + MOCK_CVV_FOUR_DIGIT, + getSDKNameAndVersion, +} from '../../src/utils/helpers'; + +// SDK identity injected by tests/jest.setup.js from this package's package.json. +declare const SDK_NAME: string; +declare const SDK_VERSION: string; + +describe('generateMockCVV', () => { + test('returns the fixed 3-digit mock for a 3-digit CVV', () => { + expect(generateMockCVV(3)).toBe(MOCK_CVV_THREE_DIGIT); + expect(generateMockCVV(3)).toBe('817'); + }); + + test('returns the fixed 4-digit mock for a 4-digit CVV', () => { + expect(generateMockCVV(4)).toBe(MOCK_CVV_FOUR_DIGIT); + expect(generateMockCVV(4)).toBe('8173'); + }); + + test('is deterministic across calls', () => { + expect(generateMockCVV(3)).toBe(generateMockCVV(3)); + expect(generateMockCVV(4)).toBe(generateMockCVV(4)); + }); + + test("returns '' for any other length", () => { + expect(generateMockCVV(0)).toBe(''); + expect(generateMockCVV(2)).toBe(''); + expect(generateMockCVV(5)).toBe(''); + }); +}); + +describe('getSDKNameAndVersion', () => { + test('returns the build-injected SDK identity when metaData is undefined', () => { + expect(getSDKNameAndVersion()).toEqual({ sdkName: SDK_NAME, sdkVersion: SDK_VERSION }); + }); + + test('returns the injected identity for an empty string', () => { + expect(getSDKNameAndVersion('')).toEqual({ sdkName: SDK_NAME, sdkVersion: SDK_VERSION }); + }); + + test('returns the injected identity when metaData has no "@" separator', () => { + expect(getSDKNameAndVersion('no-separator')).toEqual({ sdkName: SDK_NAME, sdkVersion: SDK_VERSION }); + }); + + test('parses sdkName and sdkVersion from a "name@version" metaData string', () => { + expect(getSDKNameAndVersion('skyflow-react-js@1.2.3')) + .toEqual({ sdkName: 'skyflow-react-js', sdkVersion: '1.2.3' }); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/utils/logs-helper.flowdb.test.js b/packages/skyflow-flowvault-js/tests/utils/logs-helper.flowdb.test.js new file mode 100644 index 000000000..5f4313096 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/utils/logs-helper.flowdb.test.js @@ -0,0 +1,29 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import { getStoredSdkVersion } from '@core/utils/logs-helper'; + +// In this package SDK_NAME resolves to 'skyflow-flowvault-js'. The legacy global +// `sdk_version` key is written by skyflow-react-js (which wraps skyflow-js only), +// so flowvault must NOT inherit it — otherwise its sky-metadata header and error +// logs would report skyflow-js's React identity on a shared page. +describe('Utils/logs-helper getStoredSdkVersion (skyflow-flowvault-js)', () => { + beforeEach(() => { + localStorage.clear(); + }); + + test('returns empty string when nothing is stored', () => { + expect(getStoredSdkVersion()).toBe(''); + }); + + test('ignores the legacy global sdk_version key written by skyflow-react-js', () => { + localStorage.setItem('sdk_version', 'skyflow-react-js@9.9.9'); + expect(getStoredSdkVersion()).toBe(''); + }); + + test('honours its own per-package namespaced key', () => { + localStorage.setItem('sdk_version', 'skyflow-react-js@9.9.9'); + localStorage.setItem('sdk_version:skyflow-flowvault-js', 'skyflow-flowvault-react-js@1.2.3'); + expect(getStoredSdkVersion()).toBe('skyflow-flowvault-react-js@1.2.3'); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts new file mode 100644 index 000000000..c8bd885ee --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts @@ -0,0 +1,261 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +// flowDB collect-option validators. These validate the flowDB input shapes +// (upsert: { tableName, uniqueColumns, updateType? }; additionalFields: +// { records: [{ tableName, data, skyflowId? }] }) — NOT privacyDB's +// { table, column } / { table, fields }. Guards the two regressions where the +// flowDB containers previously delegated to the privacyDB-shaped @core validators: +// B1 — a valid flowDB upsert was rejected ("Missing 'table' key ..."). +// B2 — a privacyDB-shaped additionalFields passed validation, then the impl +// dropped the data and POSTed a record keyed "undefined". +import SkyflowError from '@core/errors'; +import { + validateFlowDBUpsertOptions, + validateFlowDBAdditionalFieldsInCollect, + validateCollectElementOptions, + validateRevealElementRecords, + validateRevealOptions, + validateCollectElementInput, +} from '../../src/utils/validators'; +import { UpdateType, LogLevel } from '../../src/utils/common'; + +describe('validateFlowDBUpsertOptions', () => { + test('B1: accepts a valid flowDB upsert ({ tableName, uniqueColumns })', () => { + expect(() => validateFlowDBUpsertOptions([ + { tableName: 'cards', uniqueColumns: ['card_number'] }, + ])).not.toThrow(); + }); + + test('accepts multiple uniqueColumns and an optional updateType', () => { + expect(() => validateFlowDBUpsertOptions([ + { tableName: 'cards', uniqueColumns: ['card_number', 'cvv'], updateType: UpdateType.UPDATE }, + { tableName: 'people', uniqueColumns: ['ssn'], updateType: UpdateType.REPLACE }, + ])).not.toThrow(); + }); + + test('rejects a non-array', () => { + expect(() => validateFlowDBUpsertOptions({} as any)).toThrow(SkyflowError); + }); + + test('rejects an empty array', () => { + expect(() => validateFlowDBUpsertOptions([])).toThrow(SkyflowError); + }); + + test("rejects an entry missing 'tableName' (names tableName, at index)", () => { + expect(() => validateFlowDBUpsertOptions([{ uniqueColumns: ['card_number'] } as any])) + .toThrow(/tableName.*index 0/); + }); + + test("rejects an empty 'tableName'", () => { + expect(() => validateFlowDBUpsertOptions([{ tableName: '', uniqueColumns: ['x'] }])) + .toThrow(/tableName/); + }); + + test("rejects missing / empty / non-string 'uniqueColumns'", () => { + expect(() => validateFlowDBUpsertOptions([{ tableName: 'cards' } as any])) + .toThrow(/uniqueColumns/); + expect(() => validateFlowDBUpsertOptions([{ tableName: 'cards', uniqueColumns: [] }])) + .toThrow(/uniqueColumns/); + expect(() => validateFlowDBUpsertOptions([{ tableName: 'cards', uniqueColumns: [123] as any }])) + .toThrow(/uniqueColumns/); + }); + + test("rejects an invalid 'updateType'", () => { + expect(() => validateFlowDBUpsertOptions([ + { tableName: 'cards', uniqueColumns: ['card_number'], updateType: 'FOO' as any }, + ])).toThrow(/updateType/); + }); + + test('rejects a non-object / array / null entry (names the index)', () => { + expect(() => validateFlowDBUpsertOptions([null as any])).toThrow(/index 0/); + expect(() => validateFlowDBUpsertOptions(['cards' as any])).toThrow(/index 0/); + expect(() => validateFlowDBUpsertOptions([[] as any])).toThrow(/index 0/); + }); + + test('regression: rejects the privacyDB upsert shape ({ table, column })', () => { + expect(() => validateFlowDBUpsertOptions([{ table: 'cards', column: 'card_number' } as any])) + .toThrow(/tableName/); + }); +}); + +describe('validateFlowDBAdditionalFieldsInCollect', () => { + test('B2: accepts the flowDB record shape ({ tableName, data })', () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({ + records: [{ tableName: 'cards', data: { cvv: '123' } }], + })).not.toThrow(); + }); + + test('accepts a string skyflowId, including an empty string (treated as insert)', () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({ + records: [ + { tableName: 'cards', data: { name: 'A' }, skyflowId: 'id1' }, + { tableName: 'cards', data: { name: 'B' }, skyflowId: '' }, + ], + })).not.toThrow(); + }); + + test("rejects a missing 'records' key", () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({} as any)).toThrow(/records/); + }); + + test("rejects non-array / empty 'records'", () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({ records: {} as any })).toThrow(/records/); + expect(() => validateFlowDBAdditionalFieldsInCollect({ records: [] })).toThrow(/records/); + }); + + test("rejects a record missing / empty 'tableName' (at index)", () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({ records: [{ data: { a: 1 } } as any] })) + .toThrow(/tableName.*index 0/); + expect(() => validateFlowDBAdditionalFieldsInCollect({ + records: [{ tableName: '', data: { a: 1 } }], + })).toThrow(/tableName/); + }); + + test("rejects missing / non-object / array 'data'", () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({ records: [{ tableName: 't' } as any] })) + .toThrow(/data/); + expect(() => validateFlowDBAdditionalFieldsInCollect({ + records: [{ tableName: 't', data: [] as any }], + })).toThrow(/data/); + }); + + test("rejects a non-string 'skyflowId'", () => { + expect(() => validateFlowDBAdditionalFieldsInCollect({ + records: [{ tableName: 't', data: { a: 1 }, skyflowId: 5 as any }], + })).toThrow(/skyflowId/); + }); + + test('regression: rejects the privacyDB additionalFields shape ({ table, fields })', () => { + // Previously this passed the privacyDB validator, then the impl read tableName/data + // as undefined and POSTed { tableName: "undefined", data: {} }. Now it is rejected. + expect(() => validateFlowDBAdditionalFieldsInCollect({ + records: [{ table: 'cards', fields: { cvv: '123' } } as any], + })).toThrow(/tableName/); + }); +}); + +describe('validateCollectElementOptions', () => { + test('accepts a boolean returnMockValue', () => { + expect(() => validateCollectElementOptions({ returnMockValue: true })).not.toThrow(); + expect(() => validateCollectElementOptions({ returnMockValue: false })).not.toThrow(); + }); + + test('accepts options without returnMockValue', () => { + expect(() => validateCollectElementOptions({ required: true })).not.toThrow(); + expect(() => validateCollectElementOptions(undefined)).not.toThrow(); + }); + + test('rejects a non-boolean returnMockValue', () => { + expect(() => validateCollectElementOptions({ returnMockValue: 'true' as any })) + .toThrow(/returnMockValue/); + expect(() => validateCollectElementOptions({ returnMockValue: 1 as any })) + .toThrow(SkyflowError); + }); +}); + +describe('validateRevealElementRecords', () => { + test('accepts a valid token-only record (optional string label/altText)', () => { + expect(() => validateRevealElementRecords([{ token: 'tok-1' }])).not.toThrow(); + expect(() => validateRevealElementRecords([ + { token: 'tok-1', label: 'Card', altText: 'xxxx' } as any, + ])).not.toThrow(); + }); + + test('rejects an empty records array', () => { + expect(() => validateRevealElementRecords([])).toThrow(SkyflowError); + }); + + test("rejects a record missing the 'token' key", () => { + expect(() => validateRevealElementRecords([{} as any])).toThrow(SkyflowError); + expect(() => validateRevealElementRecords([null as any])).toThrow(SkyflowError); + }); + + test('rejects an empty token', () => { + expect(() => validateRevealElementRecords([{ token: '' }])).toThrow(SkyflowError); + }); + + test('rejects a non-string token', () => { + expect(() => validateRevealElementRecords([{ token: 123 as any }])).toThrow(SkyflowError); + }); + + test('rejects a non-string label', () => { + expect(() => validateRevealElementRecords([{ token: 'tok-1', label: 5 as any }])) + .toThrow(SkyflowError); + }); + + test('rejects a non-string altText', () => { + expect(() => validateRevealElementRecords([{ token: 'tok-1', altText: 5 as any }])) + .toThrow(SkyflowError); + }); +}); + +describe('validateRevealOptions', () => { + test('is a no-op when options are absent or tokenGroupRedactions is undefined', () => { + expect(() => validateRevealOptions()).not.toThrow(); + expect(() => validateRevealOptions({})).not.toThrow(); + }); + + test('accepts a valid tokenGroupRedactions array', () => { + expect(() => validateRevealOptions({ + tokenGroupRedactions: [{ tokenGroupName: 'grp1', redaction: 'PLAIN_TEXT' }], + })).not.toThrow(); + }); + + test('rejects a non-array tokenGroupRedactions', () => { + expect(() => validateRevealOptions({ tokenGroupRedactions: {} as any })) + .toThrow(/tokenGroupRedactions/); + }); + + test('rejects an entry with an invalid tokenGroupName (names the index)', () => { + expect(() => validateRevealOptions({ + tokenGroupRedactions: [{ tokenGroupName: '', redaction: 'PLAIN_TEXT' }], + })).toThrow(/index 0/); + expect(() => validateRevealOptions({ + tokenGroupRedactions: [{ redaction: 'PLAIN_TEXT' } as any], + })).toThrow(/index 0/); + }); + + test('rejects an entry with an invalid redaction', () => { + expect(() => validateRevealOptions({ + tokenGroupRedactions: [{ tokenGroupName: 'grp1', redaction: '' }], + })).toThrow(/index 0/); + expect(() => validateRevealOptions({ + tokenGroupRedactions: [null as any], + })).toThrow(/index 0/); + }); +}); + +describe('validateCollectElementInput', () => { + test('accepts a valid input', () => { + expect(() => validateCollectElementInput({ type: 'CARD_NUMBER' } as any, LogLevel.ERROR)) + .not.toThrow(); + }); + + test("rejects an input missing the 'type' key", () => { + expect(() => validateCollectElementInput({} as any, LogLevel.ERROR)).toThrow(SkyflowError); + }); + + test('rejects an empty type', () => { + expect(() => validateCollectElementInput({ type: '' } as any, LogLevel.ERROR)) + .toThrow(SkyflowError); + }); + + test('warns (does not throw) when the deprecated altText key is present', () => { + expect(() => validateCollectElementInput( + { type: 'CARD_NUMBER', altText: 'xxxx' } as any, LogLevel.WARN, + )).not.toThrow(); + }); + + test('rejects a non-string skyflowId', () => { + expect(() => validateCollectElementInput( + { type: 'CARD_NUMBER', skyflowId: 5 } as any, LogLevel.ERROR, + )).toThrow(SkyflowError); + }); + + test('accepts a string skyflowId', () => { + expect(() => validateCollectElementInput( + { type: 'CARD_NUMBER', skyflowId: 'id1' } as any, LogLevel.ERROR, + )).not.toThrow(); + }); +}); diff --git a/packages/skyflow-flowvault-js/tsconfig.json b/packages/skyflow-flowvault-js/tsconfig.json new file mode 100644 index 000000000..7ea3de7b0 --- /dev/null +++ b/packages/skyflow-flowvault-js/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "types" + }, + "include": ["src", "../../core/custom.d.ts", "typings.d.ts"], + "exclude": ["node_modules", "dist", "tests/*.test.*"] +} diff --git a/packages/skyflow-flowvault-js/typings.d.ts b/packages/skyflow-flowvault-js/typings.d.ts new file mode 100644 index 000000000..7f56e479f --- /dev/null +++ b/packages/skyflow-flowvault-js/typings.d.ts @@ -0,0 +1,9 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +declare module '*.json'; + +// SDK telemetry identity, injected at build time (webpack DefinePlugin) and in +// tests (jest setupFiles). flowvault supplies its own name/version. +declare const SDK_NAME: string; +declare const SDK_VERSION: string; diff --git a/packages/skyflow-flowvault-js/webpack.dev.js b/packages/skyflow-flowvault-js/webpack.dev.js new file mode 100644 index 000000000..14694bac6 --- /dev/null +++ b/packages/skyflow-flowvault-js/webpack.dev.js @@ -0,0 +1,12 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared dev-server factory (webpack/dev.js). +// Uses ports 3041/8882 (skyflow-js uses 3040/8881) so both dev servers can run +// side by side. To hit a real flowDB vault locally, add a proxy block below +// (kept local, never committed), e.g.: +// proxy: { '/vault': { target: 'https://', pathRewrite: { '^/vault': '' }, secure: false, changeOrigin: true } }, +module.exports = require('../../webpack/dev.js')(__dirname, { + port: 3041, + analyzerPort: 8882, +}); diff --git a/packages/skyflow-flowvault-js/webpack.iframe.js b/packages/skyflow-flowvault-js/webpack.iframe.js new file mode 100644 index 000000000..060264ae2 --- /dev/null +++ b/packages/skyflow-flowvault-js/webpack.iframe.js @@ -0,0 +1,5 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared iframe factory (webpack/iframe.js). +module.exports = require('../../webpack/iframe.js')(__dirname); diff --git a/packages/skyflow-flowvault-js/webpack.skyflow-browser.js b/packages/skyflow-flowvault-js/webpack.skyflow-browser.js new file mode 100644 index 000000000..d723bbbd4 --- /dev/null +++ b/packages/skyflow-flowvault-js/webpack.skyflow-browser.js @@ -0,0 +1,5 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared browser-SDK factory (webpack/browser.js). +module.exports = require('../../webpack/browser.js')(__dirname); diff --git a/packages/skyflow-flowvault-js/webpack.skyflow-node.js b/packages/skyflow-flowvault-js/webpack.skyflow-node.js new file mode 100644 index 000000000..67b42b9ab --- /dev/null +++ b/packages/skyflow-flowvault-js/webpack.skyflow-node.js @@ -0,0 +1,7 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared node-SDK factory (webpack/node.js). +// UMD global for skyflow-flowvault-js is `Skyflow` — same name as skyflow-js +// (the packages are separate bundles; alias on import for the rare dual-use). +module.exports = require('../../webpack/node.js')(__dirname, { library: 'Skyflow' }); diff --git a/packages/skyflow-js/README.md b/packages/skyflow-js/README.md new file mode 100644 index 000000000..9ccaca509 --- /dev/null +++ b/packages/skyflow-js/README.md @@ -0,0 +1,4506 @@ +# skyflow-js +Skyflow's JavaScript SDK can be used to securely collect, tokenize, and reveal sensitive data in the browser without exposing your front-end infrastructure to sensitive data. + +--- + +[![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-js/actions) +[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-js.svg)](https://www.npmjs.com/package/skyflow-js) +[![License](https://img.shields.io/github/license/skyflowapi/skyflow-js)](https://github.com/skyflowapi/skyflow-js/blob/main/LICENSE) + +## Browsers support + +| IE / Edge
    IE / Edge | Firefox
    Firefox | Chrome
    Chrome | Safari
    Safari +|--------------------------------------------------------------------------------------------------------------------------------------------------------------| --------- | --------- |-------------------------------------------------------------------------------------------------------------------------------------------------------| +# Table of Contents +- [**Including Skyflow.js**](#including-skyflowjs) +- [**Initializing Skyflow.js**](#initializing-skyflowjs) +- [**Securely collecting data client-side**](#securely-collecting-data-client-side) +- [**Securely collecting data client-side using Composable Elements**](#securely-collecting-data-client-side-using-composable-elements) +- [**Securely revealing data client-side**](#securely-revealing-data-client-side) +- [**Securely deleting data client-side**](#securely-deleting-data-client-side) +- [**Set Custom Network messages on container**](#set-custom-network-messages-on-container) +--- + +# Including Skyflow.js +Using script tag + +```html + +``` + + +Using npm + +``` +npm install skyflow-js +``` + +--- + +# Initializing Skyflow.js +Use the `init()` method to initialize a Skyflow client as shown below. +```javascript +import Skyflow from 'skyflow-js' // If using script tag, this line is not required. + +const skyflowClient = Skyflow.init({ + vaultID: 'string', // Id of the vault that the client should connect to. + vaultURL: 'string', // URL of the vault that the client should connect to. + getBearerToken: helperFunc, // Helper function that retrieves a Skyflow bearer token from your backend. + options: { + logLevel: Skyflow.LogLevel, // Optional, if not specified default is ERROR. + env: Skyflow.Env // Optional, if not specified default is PROD. + } +}); +``` +For the `getBearerToken` parameter, pass in a helper function that retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. A sample implementation is shown below: + +For example, if the response of the consumer tokenAPI is in the below format + +``` +{ + "accessToken": string, + "tokenType": string +} + +``` +then, your getBearerToken Implementation should be as below + +```javascript +const getBearerToken = () => { + return new Promise((resolve, reject) => { + const Http = new XMLHttpRequest(); + + Http.onreadystatechange = () => { + if (Http.readyState === 4) { + if (Http.status === 200) { + const response = JSON.parse(Http.responseText); + resolve(response.accessToken); + } else { + reject('Error occured'); + } + } + }; + + Http.onerror = error => { + reject('Error occured'); + }; + + const url = 'https://api.acmecorp.com/skyflowToken'; + Http.open('GET', url); + Http.send(); + }); +}; + +``` +For `logLevel` parameter, there are 4 accepted values in Skyflow.LogLevel + +- `DEBUG` + + When `Skyflow.LogLevel.DEBUG` is passed, all level of logs will be printed(DEBUG, INFO, WARN, ERROR). + +- `INFO` + + When `Skyflow.LogLevel.INFO` is passed, INFO logs for every event that has occurred during the SDK flow execution will be printed along with WARN and ERROR logs. + + +- `WARN` + + When `Skyflow.LogLevel.WARN` is passed, WARN and ERROR logs will be printed. + +- `ERROR` + + When `Skyflow.LogLevel.ERROR` is passed, only ERROR logs will be printed. + +`Note`: + - The ranking of logging levels is as follows : DEBUG < INFO < WARN < ERROR + - since `logLevel` is optional, by default the logLevel will be `ERROR`. + + + +For `env` parameter, there are 2 accepted values in Skyflow.Env + +- `PROD` +- `DEV` + + In [Event Listeners](#event-listener-on-collect-elements), actual value of element can only be accessed inside the handler when the `env` is set to `DEV`. + +`Note`: + - since `env` is optional, by default the env will be `PROD`. + - Use `env` option with caution, make sure the env is set to `PROD` when using `skyflow-js` in production. + +--- + +# Securely collecting data client-side +- [**Insert data into the vault**](#insert-data-into-the-vault) +- [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) +- [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) +- [**Bin lookup**](#bin-lookup) +- [**Using validations on Collect Elements**](#validations) +- [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) +- [**UI Error for Collect Elements**](#ui-error-for-collect-elements) +- [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) +- [**Update Collect Elements**](#update-collect-elements) +- [**Using Skyflow File Element to upload a file**](#using-skyflow-file-element-to-upload-a-file) + +## Insert data into the vault + +To insert data into the vault, use the `insert(records, options?)` method of the Skyflow client. The `records` parameter takes a JSON object of the records to insert into the below format. The `options` parameter takes an object of optional parameters for the insertion. The `insert` method also supports upsert operations. + +```javascript +const records = { + records: [ + { + table: 'string', // Table into which record should be inserted. + fields: { + column1: 'value', // Column names should match vault column names. + //...additional fields here + }, + }, + // ...additional records here. + ], +}; + +const options = { + tokens: true, // Indicates whether or not tokens should be returned for the inserted data. Defaults to 'true' + upsert: [ // Upsert operations support in the vault + { + table: 'string', // Table name + column: 'value', // Unique column in the table + } + ] +} + +skyflowClient.insert(records, options); +``` + +An [example](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/pure-js.html) of an insert call: +```javascript +skyflowClient.insert({ + records: [ + { + table: 'cards', + fields: { + cardNumber: '41111111111', + cvv: '123', + }, + }, + ], +}); +``` + +The sample response: +```javascript +{ + "records": [ + { + "table": "cards", + "fields":{ + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" + } + } + ] +} +``` + +## Update data in the vault + +To update data in the vault by skyflowID, use the `update(request, options?)` method of the Skyflow client. The request object is a JSON object describing the data to update, including the `table`, `fields`, and the `skyflowID` of the record to update. The options parameter takes an object of optional parameters for the update and includes an option to return tokenized data for the updated fields. + +```javascript +const updateRecord = { + table: 'string', // Table in which record should be updated. + fields: { + column1: 'value', // Fields to update. Column names should match vault column names. + //...additional fields here + }, + skyflowID: 'string', // The skyflow_id of the record to update. +}; + +const options = { + tokens: true, // Indicates whether or not tokens should be returned for the updated data. Defaults to 'true' +}; + +skyflowClient.update(updateRecord, options); +``` + +An [example](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/pure-update.html) of update call: +```javascript +skyflowClient.update({ + table: 'cards', + fields: { + cardNumber: '41111111111', + cvv: '123', + }, + skyflowID: '43127a6c-5c15-4513-aa15-29f50bb37182' +}); +``` + +The sample response: + +```javascript +{ + "updatedField": { + "skyflowID": "43127a6c-5c15-4513-aa15-29f50bb37182", + "cardNumber": "f390186-e7e2-466f-91e5-48e12c2bcbc1", + "cvv": "1989cb56-63da-4482-a2df-1f74cd0d1a5" + } +} +``` + +**Note**: +- The `skyflowID` field is required and should be the Skyflow ID of the record you want to update. +- If tokens is set to true, the response will include tokens for the updated fields. + +## Using Skyflow Elements to collect data + +**Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. These elements are hosted by Skyflow and injected into your web page as iFrames. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements on your web page. + +### Step 1: Create a container + +First create a container for the form elements using the `container(Skyflow.ContainerType)` method of the Skyflow client as show below: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element + +A Skyflow collect Element is defined as shown below: + +```javascript +const collectElement = { + table: 'string', // Required, the table this data belongs to. + column: 'string', // Required, the column into which this data should be inserted. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: 'string', // Optional, label for the form element. + placeholder: 'string', // Optional, placeholder for the form element. + altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. +} +``` +The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. + +**Note**: +- Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) + +The `inputStyles` field accepts a style object which consists of CSS properties that should be applied to the form element in the following states: +* `base`: all variants inherit from these styles +* `complete`: applied when the Element has valid input +* `empty`: applied when the Element has no input +* `focus`: applied when the Element has focus +* `invalid`: applied when the Element has invalid input +* `cardIcon`: applied to the card type icon in CARD_NUMBER Element +* `copyIcon`: applied to copy icon in Elements when enableCopy option is true +* `global`: used for global styles like font-family. + +Styles are specified with [JSS](https://cssinjs.org/?v=v10.7.1). + +An example of a inputStyles object: +```javascript +inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + '&:hover': { // Hover styles. + borderColor: 'green' + }, + fontFamily: '"Roboto", sans-serif' + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + copyIcon: { + position: 'absolute', + right: '8px', + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` +The states that are available for `labelStyles` are `base`, `focus`, `global` and `requiredAsterisk`. +* `requiredAsterisk`: styles applied for the Asterisk symbol in the label. + +An example of a labelStyles object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + focus: { + color: '#1d1d1d', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + requiredAsterisk:{ + color: 'red' + } +}, +``` + +The state that is available for `errorTextStyles` are `base` and `global`, it shows up when there is some error in the collect element. + +An example of a errorTextStyles object: + +```javascript +errorTextStyles: { + base: { + color: '#f44336', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +Finally, the `type` field takes a Skyflow ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 8 types: +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` +- `CVV` +- `INPUT_FIELD` +- `PIN` +- `FILE_INPUT` + + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. For information on validations, see [validations](#validations). + +Along with CollectElement we can define other options which takes a object of optional parameters as described below: + +```javascript +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether a card icon should be enabled (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon to collect elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. + cardMetadata: {}, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). + masking: true, // Optional, indicates whether the input should be masked. Defaults to 'false'. + maskingChar: '*', // Optional, character used for masking input when masking is enabled. Defaults to '*'. +}; +``` + +`required`: Indicates whether the field is marked as required or not. If not provided, it defaults to false. + +`enableCardIcon` : Indicates whether the icon is visible for the CARD_NUMBER element. Defaults to true. + +`enableCopy` : Indicates whether the copy icon is visible in collect and reveal elements. + +`format`: A string value that indicates the format pattern applicable to the element type. +Only applicable to EXPIRATION_DATE, CARD_NUMBER, EXPIRATION_YEAR, and INPUT_FIELD elements. + - For INPUT_FIELD elements, + - the length of `format` determines the expected length of the user input. + - if `translation` isn't specified, the `format` value is considered a string literal. + +`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for INPUT_FIELD elements. + +Accepted values by element type: + +| Element type | `format`and `translation` values | Examples | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | +| EXPIRATION_DATE |
  • `format`
    • `mm/yy` (default)
    • `mm/yyyy`
    • `yy/mm`
    • `yyyy/mm`
    |
    • 12/27
    • 12/2027
    • 27/12
    • 2027/12
    | +| EXPIRATION_YEAR |
  • `format`
    • `yy` (default)
    • `yyyy`
    |
    • 27
    • 2027
    | +| CARD_NUMBER |
  • `format`
    • `XXXX XXXX XXXX XXXX` (default)
    • `XXXX-XXXX-XXXX-XXXX`
    |
    • 1234 5678 9012 3456
    • 1234-5678-9012-3456
    | +| INPUT_FIELD |
  • `format`: A string that matches the desired output, with placeholder characters of your choice.
  • `translation`: An object of key/value pairs. Defaults to `{"X": "[0-9]"}`
  • | With a `format` of `+91 XXXX-XX-XXXX` and a `translation` of `[ "X": "[0-9]"]`, user input of "1234121234" displays as "+91 1234-12-1234". | + +`cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow accept card types based on which SDK will display card brand choice dropdown in the card number element. `Skyflow.CardType` is an enum with all skyflow supported card schemes. + +```javascript +import Skyflow from 'skyflow-js' + +const cardMetadata = { + scheme: Skyflow.CardType [] // Optional, array of skyflow supported card types. +} +``` + +
    Supported card types by Skyflow.CardType :
    + +- `VISA` +- `MASTERCARD` +- `AMEX` +- `DINERS_CLUB` +- `DISCOVER` +- `JCB` +- `MAESTRO` +- `UNIONPAY` +- `HIPERCARD` +- `CARTES_BANCAIRES` + +**Collect Element Options examples for INPUT_FIELD** +Example 1 +```js +const options = { + required: true, + enableCardIcon: true, + format:'+91 XXXX-XX-XXXX', + translation: { 'X': '[0-9]' } +} +``` + +User input: "1234121234" +Value displayed in INPUT_FIELD: "+91 1234-12-1234" + +Example 2 +```js +const options = { + required: true, + enableCardIcon: true, + format: 'AY XX-XXX-XXXX', + translation: { 'X': '[0-9]', 'Y': '[A-Z]' } +} +``` + +User input: "B1234121234" +Value displayed in INPUT_FIELD: "AB 12-341-2123" + +`masking` : A boolean value for whether to mask the input of the element. When masking is enabled, user input will be replaced with a masking character. +The default masking character is `*`, but you can customize masking character using the maskingChar property. + +`maskingChar`: A single character used to mask the input when masking is enabled. Defaults to `*`, but can be customized to any character of your choice. + +Collect Element Options examples with masking: + +Example for CVV: +```js +const options = { + required: true, + enableCopy: false, + masking: true, + maskingChar: '•', +} +``` +User input: "1234" +Value displayed in CVV: "••••" + +Example for CARDHOLDER_NAME: +```js +const options = { + required: true, + enableCopy: false, + masking: true, +} +``` +User input: "John Doe" +Value displayed in CARDHOLDER_NAME: "********" + +Example for CARD_NUMBER: +```js +const options = { + required: true, + enableCopy: false, + masking: true, + maskingChar: '#' +} +``` +User input: "4111 1111 1111 1111" +Value displayed in CARD_NUMBER: "#### #### #### ####" + +Example for PIN: +```js +const options = { + required: true, + enableCopy: false, + masking: true, + maskingChar: '&' +} +``` +User input: "98364721" +Value displayed in PIN: "&&&&&&&&" + +**Note**: +- Unmasked data will be stored in the vault. + +Once the Element object and options has been defined, add it to the container using the `create(element, options)` method as shown below. The `element` param takes a Skyflow Element object and options as defined above: + +```javascript +const collectElement = { + table: 'string', // Required, the table this data belongs to. + column: 'string', // Required, the column into which this data should be inserted. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: 'string', // Optional, label for the form element. + placeholder: 'string', // Optional, placeholder for the form element. + altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. +} + +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. +}; + +const element = container.create(collectElement, options); +``` + +### Step 3: Mount Elements to the DOM + +To specify where the Elements will be rendered on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has 4 empty divs with unique ids as placeholders for 4 Skyflow Elements. + +```html +
    +
    +
    +
    +
    +
    +
    +
    + + +``` + +Now, when the `mount(domElement)` method of the Element is called, the Element will be inserted in the specified div. For instance, the call below will insert the Element into the div with the id "#cardNumber". + +```javascript +element.mount('#cardNumber'); +``` +you can use the `unmount` method to reset any collect element to it's initial state. +```javascript +element.unmount(); +``` + +### Step 4: Collect data from Elements + +When the form is ready to be submitted, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: + +- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' +- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format as described in the above [Insert data into vault](#insert-data-into-the-vault) section. +- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. + +```javascript +const options = { + tokens: true, // Optional, indicates whether tokens for the collected data should be returned. Defaults to 'true'. + additionalFields: { + records: [ + { + table: 'string', // Table into which record should be inserted. + fields: { + column1: 'value', // Column names should match vault column names. + // ...additional fields here. + }, + }, + // ...additional records here. + ], + }, // Optional + upsert: [ // Upsert operations support in the vault + { + table: 'string', // Table name + column: 'value', // Unique column in the table + }, + ], // Optional +}; + +container.collect(options); +``` + +### End to end example of collecting data with Skyflow Elements + +**[Sample Code:](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/skyflow-elements.html)** + +```javascript +//Step 1 +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +//Step 2 +const element = container.create({ + table: 'cards', + column: 'cardNumber', + inputstyles: { + base: { + color: '#1d1d1d', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'Card Number', + label: 'card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Step 3 +element.mount('#cardNumber'); // Assumes there is a div with id='#cardNumber' in the webpage. + +// Step 4 + +const nonPCIRecords = { + records: [ + { + table: 'cards', + fields: { + gender: 'MALE', + }, + }, + ], +}; + +container.collect({ + tokens: true, + additionalFields: nonPCIRecords, +}); + +``` + +**Sample Response :** +```javascript +{ + "records": [ + { + "table": "cards", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e" + } + } + ] +} +``` +### Insert call example with upsert support +**Sample Code** + + ```javascript +//Step 1 +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) + +//Step 2 +const cardNumberElement = container.create({ + table: 'cards', + column: 'card_number', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon:{ + position: 'absolute', + left:'8px', + bottom:'calc(50% - 12px)' + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold' + } + }, + errorTextStyles: { + base: { + color: '#f44336' + } + }, + placeholder: 'Card Number', + label: 'card_number', + type: Skyflow.ElementType.CARD_NUMBER +}) + + +const cvvElement = container.create({ + table: 'cards', + column: 'cvv', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon:{ + position: 'absolute', + left:'8px', + bottom:'calc(50% - 12px)' + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold' + } + }, + errorTextStyles: { + base: { + color: '#f44336' + } + }, + placeholder: 'CVV', + label: 'cvv', + type: Skyflow.ElementType.CVV +}) + +// Step 3 +cardNumberElement.mount('#cardNumber') //Assumes there is a div with id='#cardNumber' in the webpage. +cvvElement.mount('#cvv'); //Assumes there is a div with id='#cvv' in the webpage. + +// Step 4 + container.collect({ + tokens: true, + upsert: [ + { + table: 'cards', + column: 'card_number', + } + ] +}) + ``` + **Skyflow returns tokens for the record you just inserted.** +```javascript +{ + "records": [ + { + "table": "cards", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e" + } + } + ] +} +``` + +## BIN Lookup + +Skyflow supports BIN (Bank Identification Number) lookup to help identify co-badged cards and enable card network selection. + +**What is BIN Lookup?** +A Bank Identification Number (BIN) represents the first 8 digits of a card number and identifies the issuing bank, card scheme, and country. +For co-badged cards, merchants are required to offer consumers a choice of which network to process the payment through. +You can use Skyflow’s BIN Lookup API to detect such cards and provide the appropriate options to users. + +### Example: Calling the BIN Lookup API +```javascript +// Function to call Skyflow's BIN Lookup API +const binLookup = (bin) => { + const myHeaders = new Headers(); + myHeaders.append("X-skyflow-authorization", ""); // TODO: replace bearer token + myHeaders.append("Content-Type", "application/json"); + + const raw = JSON.stringify({ + "BIN": bin + }); + + const requestOptions = { + method: "POST", + headers: myHeaders, + body: raw, + redirect: "follow" + }; + + // TODO: replace with your Skyflow vault URL + return fetch(`${VAULT_URL}/v1/card_lookup`, requestOptions); +}; +``` + +**Sample Response :** +```javascript +{ + "cards_data": [ + { + "BIN": "54284800", + "issuer_name": "CREDIT MUTUEL ARKEA", + "country_code": "FR", + "currency": "", + "card_type": "Credit", + "card_category": "", + "card_scheme": "CARTES BANCAIRES" + }, + { + "BIN": "54284800", + "issuer_name": "Credit Mutuel Arkea", + "country_code": "FR", + "currency": "", + "card_type": "Credit", + "card_category": "Mastercard Standard", + "card_scheme": "MASTERCARD" + } + ] +} +``` + +### Updating the Card Element with Network Schemes +```javascript +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether a card icon should be enabled (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon to collect elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. + cardMetadata: {}, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType). + masking: true, // Optional, indicates whether the input should be masked. Defaults to 'false'. + maskingChar: '*', // Optional, character used for masking input when masking is enabled. Defaults to '*'. +}; +``` + +`cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow accept card types based on which SDK will display card brand choice dropdown in the card number element. `Skyflow.CardType` is an enum with all skyflow supported card schemes. + +```javascript +import Skyflow from 'skyflow-js' + +const cardMetadata = { + scheme: Skyflow.CardType [] // Optional, array of skyflow supported card types. +} +``` + +- By default, SDK will populate its own auto-detected card scheme. + +### Samples + +- [Card brand choice](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/card-brand-choice.html): +This sample illustrates how to use Bin Lookup API and display the available card schemes. + +## Using Skyflow Elements to update data + +You can update the data in a vault with Skyflow Elements. Use the following steps to securely update data. + +### Step 1: Create a container +Create a container for the form elements using the `container(Skyflow.ContainerType)` method of the Skyflow client: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a collect Element +Create a collect element. Collect Elements are defined as follows: + +```javascript +const collectElement = { + table: "string", // Required, the table this data belongs to. + column: "string", // Required, the column into which this data should be updated. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: "string", // Optional, label for the form element. + placeholder: "string", // Optional, placeholder for the form element. + altText: "string", // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. + skyflowID: "string", // The skyflow_id of the record to be updated. +}; +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether the element needs a card icon (only applicable for CARD_NUMBER ElementType). + enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {}, // Optional, indicates the allowed data type value for format. +}; +const element = container.create(collectElement, options); +``` +The `table` and `column` fields indicate which table and column the Element corresponds to. + +`skyflowID` indicates the record that you want to update. + +**Notes:** +- Use dot-delimited strings to specify columns nested inside JSON fields (for example, `address.street.line1`) + +### Step 3: Mount Elements to the DOM +To specify where the Elements are rendered on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has three empty elements with unique IDs as placeholders for three Skyflow Elements. +```html +
    +
    +
    +
    +
    +
    +
    + + +``` +Now, when you call the `mount(domElement)` method, the Elements is inserted in the specified divs. For instance, the call below inserts the Element into the div with the id "#cardNumber". +```javascript +element.mount('#cardNumber'); +``` +Use the `unmount` method to reset a Collect Element to its initial state. +```javascript +element.unmount(); +``` + + +### Step 4: Update data from Elements +When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: +- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' +- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. +- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. + +```javascript +const options = { + tokens: true, // Optional, indicates whether tokens for the collected data should be returned. Defaults to 'true'. + additionalFields: { + records: [ + { + table: "string", // Table into which record should be updated. + fields: { + column1: "value", // Column names should match vault column names. + skyflowID: "value", // The skyflow_id of the record to be updated. + // ...additional fields here. + }, + }, + // ...additional records here. + ], + },// Optional + upsert: [ // Upsert operations support in the vault + { + table: "string", // Table name + column: "value", // Unique column in the table + }, + ], // Optional +}; +container.collect(options); +``` +**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. + +### End to end example of updating data with Skyflow Elements + +**Sample Code:** + +```javascript +//Step 1 +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +//Step 2 +const cardNumberElement = container.create({ + table: 'cards', + column: 'cardNumber', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'Card Number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', +}); +const cardHolderNameElement = container.create({ + table: 'cards', + column: 'first_name', + inputStyles: { + base: { + color: '#1d1d1d', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'Card Holder Name', + label: 'Card Holder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', +}); + +// Step 3 +cardNumberElement.mount('#cardNumber'); // Assumes there is a div with id='#cardNumber' in the webpage. +cardHolderNameElement.mount('#cardHolderName'); // Assumes there is a div with id='#cardHolderName' in the webpage. + +// Step 4 +const nonPCIRecords = { + records: [ + { + table: 'cards', + fields: { + gender: 'MALE', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + }, + }, + ], +}; + +container.collect({ + tokens: true, + additionalFields: nonPCIRecords, +}); +``` +**Sample Response :** +```javascript +{ + "records": [ + { + "table": "cards", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", + "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e" + } + } + ] +} +``` + +### Validations + +Skyflow-JS provides two types of validations on Collect Elements + +#### 1. Default Validations: +Every Collect Element except of type `INPUT_FIELD` has a set of default validations listed below: +- `CARD_NUMBER`: Card number validation with checkSum algorithm(Luhn algorithm). +Available card lengths for defined card types are [12, 13, 14, 15, 16, 17, 18, 19]. +A valid 16 digit card number will be in the format - `XXXX XXXX XXXX XXXX` +- `CARD_HOLDER_NAME`: Name should be 2 or more symbols, valid characters should match pattern - `^([a-zA-Z\\ \\,\\.\\-\\']{2,})$` +- `CVV`: Card CVV can have 3-4 digits +- `EXPIRATION_DATE`: Any date starting from current month. By default valid expiration date should be in short year format - `MM/YY` +- `PIN`: Can have 4-12 digits + +#### 2. Custom Validations: +Custom validations can be added to any element which will be checked after the default validations have passed. The following Custom validation rules are currently supported: +- `REGEX_MATCH_RULE`: You can use this rule to specify any Regular Expression to be matched with the input field value + +```javascript +const regexMatchRule = { + type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE, + params: { + regex: RegExp, + error: string // Optional, default error is 'VALIDATION FAILED'. + } +} +``` + +- `LENGTH_MATCH_RULE`: You can use this rule to set the minimum and maximum permissible length of the input field value + +```javascript +const lengthMatchRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min : number, // Optional. + max : number, // Optional. + error: string // Optional, default error is 'VALIDATION FAILED'. + } +} +``` + +- `ELEMENT_VALUE_MATCH_RULE`: You can use this rule to match the value of one element with another element + +```javascript +const elementValueMatchRule = { + type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + element: CollectElement, + error: string // Optional, default error is 'VALIDATION FAILED'. + } +} +``` + +The Sample [code snippet](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/custom-validations.html) for using custom validations: + +```javascript +/* + A simple example that illustrates custom validations. + Adding REGEX_MATCH_RULE , LENGTH_MATCH_RULE to collect element. +*/ + +// This rule allows 1 or more alphabets. +const alphabetsOnlyRegexRule = { + type: Skyflow.ValidationRuleType.REGEX_MATCH_RULE, + params: { + regex: /^[A-Za-z]+$/, + error: 'Only alphabets are allowed', + }, +}; + +// This rule allows input length between 4 and 6 characters. +const lengthRule = { + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 4, + max: 6, + error: 'Must be between 4 and 6 alphabets', + }, +}; + +const cardHolderNameElement = collectContainer.create({ + table: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + label: 'Card Holder Name', + placeholder: 'cardholder name', + type: Skyflow.ElementType.INPUT_FIELD, + validations: [alphabetsOnlyRegexRule, lengthRule], +}); + +/* + Reset PIN - A simple example that illustrates custom validations. + The below code shows an example of ELEMENT_VALUE_MATCH_RULE +*/ + +// For the PIN element +const pinElement = collectContainer.create({ + label: 'PIN', + placeholder: '****', + type: Skyflow.ElementType.PIN, +}); + +// This rule allows to match the value with pinElement. +const elementMatchRule = { + type: Skyflow.ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + element: pinElement, + error: 'PIN does not match', + }, +}; + +const confirmPinElement = collectContainer.create({ + label: 'Confirm PIN', + placeholder: '****', + type: Skyflow.ElementType.PIN, + validations: [elementMatchRule], +}); + +// Mount elements on screen - errors will be shown if any of the validaitons fail. +pinElement.mount('#collectPIN'); +confirmPinElement.mount('#collectConfirmPIN'); + +``` +### Event Listener on Collect Elements + + +Helps to communicate with Skyflow elements / iframes by listening to an event + +```javascript +element.on(Skyflow.EventName,handler:function) +``` + +There are 4 events in `Skyflow.EventName` +- `CHANGE` + Change event is triggered when the Element's value changes. + +- `READY` + Ready event is triggered when the Element is fully rendered + +- `FOCUS` + Focus event is triggered when the Element gains focus + +- `BLUR` + Blur event is triggered when the Element loses focus. + +The handler ```function(state) => void``` is a callback function you provide, that will be called when the event is fired with the state object as shown below. + +```javascript +state : { + elementType: Skyflow.ElementType + isEmpty: boolean + isFocused: boolean + isValid: boolean + value: string + selectedCardScheme: Skyflow.CardType // only for CARD_NUMBER element type +} +``` + +**Note:** +- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. +- `selectedCardScheme` will exist for `CARD_NUMBER` element state and the value of Skyflow.CardType will be only populated when cardbrand choice selection is triggered otherwise, it will always be an empty string. + +##### Sample [code snippet](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/collect-element-listeners.html) for using listeners +```javascript +// Create Skyflow client. +const skyflowClient = Skyflow.init({ + vaultID: '', + vaultURL: '', + getBearerToken: () => {}, + options: { + env: Skyflow.Env.DEV, + }, +}); + +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardHolderName = container.create({ + table: 'pii_fields', + column: 'first_name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); +const cardNumber = container.create({ + table: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +cardNumber.mount('#cardNumberContainer'); +cardHolderName.mount('#cardHolderNameContainer'); + +// Subscribing to CHANGE event, which gets triggered when element changes. +cardHolderName.on(Skyflow.EventName.CHANGE, state => { + // Your implementation when Change event occurs. + console.log(state); +}); + +// Subscribing to CHANGE event, which gets triggered when element changes. +cardNumber.on(Skyflow.EventName.CHANGE, state => { + // Your implementation when Change event occurs. + console.log(state); +}); + +``` +##### Sample Element state object when `env` is `DEV` + +```javascript +{ + elementType: 'CARDHOLDER_NAME', + isEmpty: false, + isFocused: true, + isValid: false, + value: 'John', +}; +{ + elementType: 'CARD_NUMBER', + isEmpty: false, + isFocused: true, + isValid: false, + value: '4111-1111-1111-1111', +}; +``` +##### Sample Element state object when `env` is `PROD` + +```javascript +{ + elementType: 'CARDHOLDER_NAME', + isEmpty: false, + isFocused: true, + isValid: false, + value: '', +}; +{ + elementType: 'CARD_NUMBER', + isEmpty: false, + isFocused: true, + isValid: false, + value: '4111-1111-XXXX-XXXX', +}; + +``` + +### UI Error for Collect Elements + +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: string)` method is used to set the error text for the element, when this method is triggered, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is triggered on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +##### Sample code snippet for setError and resetError + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + table: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Set custom error. +cardNumber.setError('custom error'); + +// Reset custom error. +cardNumber.resetError(); +``` + +### Override default error Messages + +You can override the default error messages with custom ones by using `setErrorOverride`. This is especially useful to override default error messages in non-English languages. + +`setErrorOverride(message: string)` + +`setErrorOverride` overrides the default error message. When the value is invalid, the error resets automatically when the value becomes valid. + +##### Sample code snippet for setErrorOverride + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + table: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// override default error. +cardHolderNameElement.on(Skyflow.EventName.BLUR, state=>{ + if(state.isEmpty) { + //can override the message when the field is required and empty + cardHolderNameElement.setErrorOverride('custom error for required'); + } else if(!state.isValid) { + //can override the message when the input is invalid + cardHolderName.setErrorOverride('custom error for invalid'); + } +}); +``` + +##### Difference between setError and setErrorOverride: + +- `setError` sets the error state on the collect element, regardless of the element's state and value (valid or invalid). Once you call `setError`, the element remains in the error state until you call `resetError`. Use `setError` to set the error state on collect element based on server-side validations. + +- `setErrorOverride` overrides the default error message. The error message resets automatically once the value becomes valid. Use `setErrorOverride` to change the default error message for a collect element. + +**Note**: +- `setErrorOverride` can only override default error messages. +- `setErrorOverride` can only be used in BLUR event listener as shown in the earlier example. + + +### Set and Clear value for Collect Elements (DEV ENV ONLY) + +`setValue(value: string)` method is used to set the value of the element. This method will override any previous value present in the element. + +`clearValue()` method is used to reset the value of the element. + +`Note:` This methods are only available in DEV env for testing/developmental purposes and MUST NOT be used in PROD env. + +##### Sample code snippet for setValue and clearValue + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + table: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Set a value programatically. +cardNumber.setValue('4111111111111111'); + +// Clear the value. +cardNumber.clearValue(); + +``` + +### Update Collect Elements + +You can update collect element properties with the `update` interface. + +The `update` interface takes the below object: + +```javascript +const updateElement = { + table: 'string', // Optional. The table this data belongs to. + column: 'string', // Optional. The column this data belongs to. + inputStyles: {}, // Optional. Styles applied to the form element. + labelStyles: {}, // Optional. Styles for the label of the element. + errorTextStyles: {}, // Optional. Styles for the errorText of element. + label: 'string', // Optional. Label for the form element. + placeholder: 'string', // Optional. Placeholder for the form element. + validations: [], // Optional. Array of validation rules. + skyflowID: 'string' // Optional. SkyflowID of the record. +}; +``` + +Only include the properties that you want to update for the specified collect element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +`Note`: You can't update the `type` property of an element. + +### End to end example +```javascript +// Create a collect container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: {}, + }, +}; + +// Create collect elements +const cardHolderNameElement = collectContainer.create({ + table: 'pii_fields', + column: 'first_name', + ...stylesOptions, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); + +const cardNumberElement = collectContainer.create({ + table: 'pii_fields', + column: 'card_number', + ...stylesOptions, + placeholder: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const cvvElement = collectContainer.create({ + table: 'pii_fields', + column: 'cvv', + ...stylesOptions, + placeholder: 'CVV', + type: Skyflow.ElementType.CVV, +}); + +// Mount the collect elements. +cardHolderNameElement.mount('#cardHolderNameElement'); // Assumes there is a div with id='#cardHolderNameElement' in the webpage. +cardNumberElement.mount('#cardNumberElement'); // Assumes there is a div with id='#cardNumberElement' in the webpage. +cvvElement.mount('#cvvElement'); // Assumes there is a div with id='#cvvElement' in the webpage. + +// ... + +// Update validations property on cvvElement. +cvvElement.update({ + validations: [{ + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: 'cvv must be 3 digits', + }, + }] +}) + +// Update label, placeholder properties on cardHolderNameElement. +cardHolderNameElement.update({ + label: 'CARDHOLDER NAME', + placeholder: 'Eg: John' +}); + +// Update table, column, inputStyles properties on cardNumberElement. +cardNumberElement.update({ + table:'cards', + column:'card_number', + inputStyles:{ + base:{ + color:'blue' + } + } +}); +``` + +--- + + +## Using Skyflow File Element to upload a file + +You can upload binary files to a vault using the Skyflow File Element. Use the following steps to securely upload a file. +### Step 1: Create a container + +Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT) +``` + +### Step 2: Create a File Element + +Skyflow Collect Elements are defined as follows: + +```javascript +const collectElement = { + type: Skyflow.ElementType.FILE_INPUT, // Skyflow.ElementType enum. + table: 'string', // The table this data belongs to. + column: 'string', // The column into which this data should be inserted. + skyflowID: 'string', // The skyflow_id of the record. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles:{}, // Optional, styles that will be applied to the errorText of the collect element. +} +``` +The `table` and `column` fields indicate which table and column the Element corresponds to. + +`skyflowID` indicates the record that stores the file. + +**Notes**: +- `skyflowID` is required while creating File element +- Use period-delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`). + +### Step 3: Mount elements to the DOM + +To specify where to render Elements on your page, create placeholder `
    ` elements with unique `id` tags. For instance, the form below has an empty div with a unique id as a placeholder for a Skyflow Element. + +```html +
    +
    +
    + + +``` + +Now, when the `mount(domElement)` method of the Element is called, the Element is inserted in the specified div. For instance, the call below inserts the Element into the div with the id "#file". + +```javascript +element.mount('#file'); +``` +Use the `unmount` method to reset a Collect Element to its initial state. + +```javascript +element.unmount(); +``` +### Step 4: Collect data from elements + +When you're ready to upload the file, call the `uploadFiles()` method on the container object. + +```javascript +container.uploadFiles(); +``` +### File upload limitations: + +- Only non-executable file are allowed to be uploaded. +- Files must have a maximum size of 32 MB +- File columns can't enable tokenization, redaction, or arrays. +- Re-uploading a file overwrites previously uploaded data. +- Partial uploads or resuming a previous upload isn't supported. + +### End-to-end file upload + +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +// Step 2. +const element = container.create({ + table: 'pii_fields', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.FILE_INPUT, +}); + +// Step 3. +element.mount('#file'); // Assumes there is a div with id='#file' in the webpage. + +// Step 4. +container.uploadFiles(); +``` + +**Sample Response :** +```javascript +{ + fileUploadResponse: [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` +### File upload with options: + +Along with fileElementInput, you can define other options in the Options object as described below: +```js +const options = { + allowedFileType: String[], // Optional, indicates the allowed file types for upload +} +``` +`allowedFileType`: An array of string value that indicates the allowedFileTypes to be uploaded. + +#### File upload with options example + +```javascript +// Create collect Container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +// Create collect elements. +const cardNumberElement = collectContainer.create({ + table: 'newTable', + column: 'card_number', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); +const options = { + allowedFileType: [".pdf",".png"]; +}; +const fileElement = collectContainer.create({ + table: 'newTable', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.FILE_INPUT, +}, + options +); + +// Mount the elements. +cardNumberElement.mount('#collectCardNumber'); +fileElement.mount('#collectFile'); + +// Collect and upload methods. +collectContainer.collect({}); +collectContainer.uploadFiles(); + +``` +**Sample Response for collect():** +```javascript +{ + "records": [ + { + "table": "newTable", + "fields": { + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + } + } + ] +} +``` +**Sample Response for file uploadFiles() :** +```javascript +{ + "fileUploadResponse": [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` +#### File upload with additional elements + +```javascript +// Create collect Container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +// Create collect elements. +const cardNumberElement = collectContainer.create({ + table: 'newTable', + column: 'card_number', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const fileElement = collectContainer.create({ + table: 'newTable', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.FILE_INPUT, +}); + +// Mount the elements. +cardNumberElement.mount('#collectCardNumber'); +fileElement.mount('#collectFile'); + +// Collect and upload methods. +collectContainer.collect({}); +collectContainer.uploadFiles(); + +``` +**Sample Response for collect():** +```javascript +{ + "records": [ + { + "table": "newTable", + "fields": { + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + } + } + ] +} +``` +**Sample Response for file uploadFiles() :** +```javascript +{ + "fileUploadResponse": [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` + +Note: File name should contain only alphanumeric characters and !-_.*() + +# Securely collecting data client-side using Composable Elements +- [**Using Skyflow Composable Elements to collect data**](#using-skyflow-composable-elements-to-collect-data) +- [**Event listener on Composable Element**](#set-an-event-listener-on-composable-elements) +- [**Event listener on Composable Container**](#set-an-event-listener-on-a-composable-container) +- [**Update Composable Elements**](#update-composable-elements) +- [**Using Skyflow File Element to upload a file**](#using-skyflow-composable-file-element-to-upload-a-file) +- [**Using Skyflow File Element to upload multiple files**](#using-skyflow-composable-file-element-to-upload-multiple-files) + + +## Using Skyflow Composable Elements to collect data +Composable Elements combine multiple Skyflow Elements in a single iframe, letting you create multiple Skyflow Elements in a single row. The following steps create a composable element and securely collect data through it. + +### Step 1: Create a composable container + +Create a container for the composable element using the `container(Skyflow.ContainerType)` method of the Skyflow client: + +``` javascript + const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE,containerOptions); +``` +Pass an options object that contains the following keys: + +1. `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. + + For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. + + `Note`: The sum of values in the layout array should be equal to the number of elements created + +2. `styles`: CSS styles to apply to the composable container. +3. `errorTextStyles`: CSS styles to apply if an error is encountered. + +```javascript +const options = { + layout: [2, 1], // Required + styles: { // Optional + base: { + border: '1px solid #DFE3EB', + padding: '8px', + borderRadius: '4px', + margin: '12px 2px', + }, + }, + errorTextStyles: { // Optional + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, +}; +``` + +### Step 2: Create Composable Elements +Composable Elements use the following schema: + +```javascript +const composableElement = { + table: 'string', // Required. The table this data belongs to. + column: 'string', // Required. The column this data belongs to. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional. Styles applied to the form element. + labelStyles: {}, // Optional. Styles for the label of the collect element. + errorTextStyles: {}, // Optional. Styles for the errorText of the collect element. + label: 'string', // Optional. Label for the form element. + placeholder: 'string', // Optional. Placeholder for the form element. + altText: 'string', // (DEPRECATED) Initial value for the collect element. + validations: [], // Optional. Array of validation rules. +} +``` +The `table` and `column` fields indicate which table and column in the vault the Element correspond to. + +Note: Use dot-delimited strings to specify columns nested inside JSON fields (for example, `address.street.line1`). + +All elements can be styled with [JSS](https://cssinjs.org/?v=v10.7.1) syntax. + +The `inputStyles` field accepts an object of CSS properties to apply to the form element in the following states: + +* `base`: all variants inherit from these styles +* `complete`: applied when the Element has valid input +* `empty`: applied when the Element has no input +* `focus`: applied when the Element has focus +* `invalid`: applied when the Element has invalid input +* `cardIcon`: applied to the card type icon in CARD_NUMBER Element +* `copyIcon`: applied to copy icon in Elements when enableCopy option is true +* `global`: used for global styles like font-family. + +An example of an `inputStyles` object: + +```javascript +inputStyles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + color: '#1d1d1d', + fontFamily: '"Roboto", sans-serif' + }, + complete: { + color: '#4caf50', + }, + empty: {}, + focus: {}, + invalid: { + color: '#f44336', + }, + cardIcon: { + position: 'absolute', + left: '8px', + bottom: 'calc(50% - 12px)', + }, + copyIcon: { + position: 'absolute', + right: '8px', + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` +The states that are available for `labelStyles` are `base`, `focus`, `global`. +* requiredAsterisk: styles applied for the Asterisk symbol in the label. + +An example `labelStyles` object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + fontFamily: '"Roboto", sans-serif' + }, + focus: { + color: '#1d1d1d' + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` + +The JS SDK supports the following composable elements: + +- `CARDHOLDER_NAME` +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` +- `CVV` +- `INPUT_FIELD` +- `PIN` + +`Note`: Only when the entered value in the below composable elements is valid, the focus shifts automatically. The element types are: +- `CARD_NUMBER` +- `EXPIRATION_DATE` +- `EXPIRATION_MONTH` +- `EXPIRATION_YEAR` + +The `INPUT_FIELD` type is a custom UI element without any built-in validations. For information on validations, see [validations](#validations). + +Along with the Composable Element definition, you can define additional options for the element: + +```javascript +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType) + format: String, // Optional, format for the element (only applicable currently for EXPIRATION_DATE ElementType), + enableCopy: false // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false') +} +``` + +- `required`: Whether or not the field is marked as required. Defaults to `false`. +- `enableCardIcon`: Whether or not the icon is visible for the CARD_NUMBER element. Defaults to `true`. +- `format`: Format pattern for the element. Only applicable to EXPIRATION_DATE and EXPIRATION_YEAR element types. +- `enableCopy`: Whether or not the copy icon is visible in collect and reveal elements. Defaults to `false`. + +The accepted `EXPIRATION_DATE` values are + +- `MM/YY` (default) +- `MM/YYYY` +- `YY/MM` +- `YYYY/MM` + + +The accepted `EXPIRATION_YEAR` values are + +- `YY` (default) +- `YYYY` + + +Once you define the Element object and options, add it to the container using the `create(element, options)` method: + +```javascript +const composableElement = { + table: 'string', // Required, the table this data belongs to. + column: 'string', // Required, the column into which this data should be inserted. + type: Skyflow.ElementType, // Skyflow.ElementType enum. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the collect element. + label: 'string', // Optional, label for the form element. + placeholder: 'string', // Optional, placeholder for the form element. + altText: 'string', // (DEPRECATED) string that acts as an initial value for the collect element. + validations: [], // Optional, array of validation rules. +} + +const options = { + required: false, // Optional, indicates whether the field is marked as required. Defaults to 'false'. + enableCardIcon: true, // Optional, indicates whether card icon should be enabled (only applicable for CARD_NUMBER ElementType). + format: String, // Optional, format for the element (only applicable currently for EXPIRATION_DATE ElementType). + enableCopy: false, // Optional, enables the copy icon in collect and reveal elements to copy text to clipboard. Defaults to 'false'). +}; + +const element = container.create(composableElement, options); +``` + +### Step 3: Mount Container to the DOM +To specify where the Elements are rendered on your page, create a placeholder `
    ` element with unique `id` attribute. Use this empty `
    ` placeholder to mount the composable container. + +```javascript +
    +
    +
    +
    + + +``` +Use the composable container's `mount(domElement)` method to insert the container's Elements into the specified `
    `. For instance, the following call inserts Elements into the `
    ` with the `id "#composableContainer"`. + +```javacript +container.mount('#composableContainer'); +``` + +### Step 4: Collect data from elements + + +When the form is ready to be submitted, call the container's `collect(options?)` method. The options parameter takes an object of optional parameters as follows: +- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' +- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. +- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. + +```javascript +const options = { + tokens: true, // Optional, indicates whether tokens for the collected data should be returned. Defaults to 'true'. + additionalFields: { + records: [ + { + table: 'string', // Table into which record should be inserted. + fields: { + column1: 'value', // Column names should match vault column names. + // ...additional fields here. + }, + }, + // ...additional records here. + ], + }, // Optional + upsert: [ // Upsert operations support in the vault + { + table: 'string', // Table name + column: 'value', // Unique column in the table + }, + ], // Optional +}; +``` + +### End to end example of collecting data with Composable Elements + +```javascript +// Step 1 +const containerOptions = { + layout: [2, 1], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + }, + }, + errorTextStyles: { + base: { + color: 'red', + }, + }, +}; + +const composableContainer = skyflowClient.container( + Skyflow.ContainerType.COMPOSABLE, + containerOptions +); + +// Step 2 + +const collectStylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: {}, + }, +}; + +const cardHolderNameElement = composableContainer.create({ + table: 'pii_fields', + column: 'first_name', + ...collectStylesOptions, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); + +const cardNumberElement = composableContainer.create({ + table: 'pii_fields', + column: 'card_number', + ...collectStylesOptions, + placeholder: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const cvvElement = composableContainer.create({ + table: 'pii_fields', + column: 'cvv', + ...collectStylesOptions, + placeholder: 'CVV', + type: Skyflow.ElementType.CVV, +}); + +// Step 3 +composableContainer.mount('#composableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. + +// Step 4 +composableContainer.collect({ + tokens: true, +}); +``` +### Sample Response: + +```javascript +{ + "records": [ + { + "table": "pii_fields", + "fields": { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", + "first_name": "63b5eeee-3624-493f-825e-137a9336f882", + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "cvv": "7baf5bda-aa22-4587-a5c5-412f6f783a19", + } + } + ] +} +``` +For information on validations, see [validations](#validations). + +### Set an event listener on Composable Elements: + +You can communicate with Skyflow Elements by listening to element events: + +```javascript +element.on(Skyflow.EventName,handler:function) +``` + + +The SDK supports four events: + +- `CHANGE`: Triggered when the Element's value changes. +- `READY`: Triggered when the Element is fully rendered. +- `FOCUS`: Triggered when the Element gains focus. +- `BLUR`: Triggered when the Element loses focus. + +The handler `function(state) => void` is a callback function you provide that's called when the event is fired with a state object that uses the following schema: + +```javascript +state : { + elementType: Skyflow.ElementType + isEmpty: boolean + isFocused: boolean + isValid: boolean + value: string +} +``` +`Note`: Events only include element values when in the state object when env is DEV. By default, value is an empty string. + +### Example Usage of Event Listener on Composable Elements + +```javascript +const containerOptions = { + layout: [1], + styles: { + base: { + border: '1px solid #eae8ee', + padding: '10px 16px', + borderRadius: '4px', + margin: '12px 2px', + } + }, + errorTextStyles: { + base: { + color: 'red' + } + } +} + +const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +const cvv = composableContainer.create({ + table: 'pii_fields', + column: 'primary_card.cvv', + type: Skyflow.ElementType.CVV, +}); + +composableContainer.mount('#cvvContainer'); + +// Subscribing to CHANGE event, which gets triggered when element changes. +cvv.on(Skyflow.EventName.CHANGE, state => { +// Your implementation when Change event occurs. +console.log(state); +}); +``` + +Sample Element state object when env is `DEV` + +```javascript +{ + elementType: 'CVV' + isEmpty: false + isFocused: true + isValid: false + value: '411' +} +``` + +Sample Element state object when env is `PROD` + +```javascript +{ + elementType: 'CVV' + isEmpty: false + isFocused: true + isValid: false + value: '' +} +``` + +### Update composable elements +You can update composable element properties with the `update` interface. + + +The `update` interface takes the below object: +```javascript +const updateElement = { + table: 'string', // Optional. The table this data belongs to. + column: 'string', // Optional. The column this data belongs to. + inputStyles: {}, // Optional. Styles applied to the form element. + labelStyles: {}, // Optional. Styles for the label of the element. + errorTextStyles: {}, // Optional. Styles for the errorText of element. + label: 'string', // Optional. Label for the form element. + placeholder: 'string', // Optional. Placeholder for the form element. + validations: [], // Optional. Array of validation rules. +}; +``` + +Only include the properties that you want to update for the specified composable element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +`Note`: You can't update the `type` property of an element. + +### End to end example +```javascript +const containerOptions = { layout: [2, 1] }; + +// Create a composable container. +const composableContainer = skyflowClient.container( + Skyflow.ContainerType.COMPOSABLE, + containerOptions +); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: {}, + }, +}; + +// Create composable elements. +const cardHolderNameElement = composableContainer.create({ + table: 'pii_fields', + column: 'first_name', + ...stylesOptions, + placeholder: 'Cardholder Name', + type: Skyflow.ElementType.CARDHOLDER_NAME, +}); + + +const cardNumberElement = composableContainer.create({ + table: 'pii_fields', + column: 'card_number', + ...stylesOptions, + placeholder: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const cvvElement = composableContainer.create({ + table: 'pii_fields', + column: 'cvv', + ...stylesOptions, + placeholder: 'CVV', + type: Skyflow.ElementType.CVV, +}); + +// Mount the composable container. +composableContainer.mount('#compostableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. + +// ... + +// Update validations property on cvvElement. +cvvElement.update({ + validations: [{ + type: Skyflow.ValidationRuleType.LENGTH_MATCH_RULE, + params: { + max: 3, + error: 'cvv must be 3 digits', + }, + }] +}) + +// Update label, placeholder properties on cardHolderNameElement. +cardHolderNameElement.update({ + label: 'CARDHOLDER NAME', + placeholder: 'Eg: John' +}); + +// Update table, column, inputStyles properties on cardNumberElement. +cardNumberElement.update({ + table:'cards', + column:'card_number', + inputStyles:{ + base:{ + color:'blue' + } + } +}); + + +``` +### Set an event listener on a composable container +Currently, the SDK supports one event: +- `SUBMIT`: Triggered when the `Enter` key is pressed in any container element. + +The handler `function(void) => void` is a callback function you provide that's called when the `SUBMIT' event fires. + +### Example +```javascript +const containerOptions = { layout: [1] } + +// Creating a composable container. +const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Creating the element. +const cvv = composableContainer.create({ + table: 'pii_fields', + column: 'primary_card.cvv', + type: Skyflow.ElementType.CVV, +}); + +// Mounting the container. +composableContainer.mount('#cvvContainer'); + +// Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. +composableContainer.on(Skyflow.EventName.SUBMIT, ()=> { + // Your implementation when the SUBMIT(enter) event occurs. + console.log('Submit Event Listener is being Triggered.'); +}); +``` + +## Using Skyflow Composable File Element to upload a file +You can upload binary files to a vault using the Skyflow File Element. Use the following steps to securely upload a file. +### Step 1: Create a container + +Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: + +```javascript +const containerOptions = { layout: [1] } + +// Creating a composable container. +const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); +``` + +### Step 2: Create a File Element + +Skyflow Collect Elements are defined as follows: + +```javascript +const collectElement = { + type: Skyflow.ElementType.FILE_INPUT, // Skyflow.ElementType enum. + table: 'string', // The table this data belongs to. + column: 'string', // The column into which this data should be inserted. + skyflowID: 'string', // The skyflow_id of the record. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles:{}, // Optional, styles that will be applied to the errorText of the collect element. +} +``` +The `table` and `column` fields indicate which table and column the Element corresponds to. + +`skyflowID` indicates the record that stores the file. + +**Notes**: +- `skyflowID` is required while creating File element +- Use period-delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`). + +### Step 3: Mount Container to the DOM +Mount Elements for file upload to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-container-to-the-dom). + +### Step 4: Collect data from elements + +When you're ready to upload the file, call the `uploadFiles()` method on the container object. + +```javascript +composableContainer.uploadFiles(); +``` +### File upload limitations: + +- Only non-executable file are allowed to be uploaded. +- Files must have a maximum size of 32 MB +- File columns can't enable tokenization, redaction, or arrays. +- Re-uploading a file overwrites previously uploaded data. +- Partial uploads or resuming a previous upload isn't supported. + +### End-to-end file upload + +```javascript +// Step 1. +const containerOptions = { layout: [1] } + +// Creating a composable container. +const container = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Step 2. +const element = container.create({ + table: 'pii_fields', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.FILE_INPUT, +}); + +// Step 3. +container.mount('#file'); // Assumes there is a div with id='#file' in the webpage. + +// Step 4. +container.uploadFiles(); +``` + +**Sample Response :** +```javascript +{ + fileUploadResponse: [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` +### File upload with options: + +Along with fileElementInput, you can define other options in the Options object as described below: +```js +const options = { + allowedFileType: String[], // Optional, indicates the allowed file types for upload +} +``` +`allowedFileType`: An array of string value that indicates the allowedFileTypes to be uploaded. + +#### File upload with options example + +```javascript +// Create collect Container. +const containerOptions = { layout: [1] } + +// Creating a composable container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Create collect elements. +const cardNumberElement = collectContainer.create({ + table: 'newTable', + column: 'card_number', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); +const options = { + allowedFileType: [".pdf",".png"]; +}; +const fileElement = collectContainer.create({ + table: 'newTable', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.FILE_INPUT, +}, + options +); + +// Mount the elements. +collectContainer.mount('#collectContainer'); + +// Collect and upload methods. +collectContainer.collect({}); +collectContainer.uploadFiles(); + +``` +**Sample Response for collect():** +```javascript +{ + "records": [ + { + "table": "newTable", + "fields": { + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + } + } + ] +} +``` +**Sample Response for file uploadFiles() :** +```javascript +{ + "fileUploadResponse": [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` +#### File upload with additional elements + +```javascript +// Create collect Container. +const containerOptions = { layout: [1,1] } + +// Creating a composable container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Create collect elements. +const cardNumberElement = collectContainer.create({ + table: 'newTable', + column: 'card_number', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const fileElement = collectContainer.create({ + table: 'newTable', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.FILE_INPUT, +}); + +// Mount the elements. +cardNumberElement.mount('#collectCardNumber'); +fileElement.mount('#collectFile'); + +// Collect and upload methods. +collectContainer.collect({}); +collectContainer.uploadFiles(); + +``` +**Sample Response for collect():** +```javascript +{ + "records": [ + { + "table": "newTable", + "fields": { + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + } + } + ] +} +``` +**Sample Response for file uploadFiles() :** +```javascript +{ + "fileUploadResponse": [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` + +Note: File name should contain only alphanumeric characters and !-_.*() + + +## Using Skyflow Composable File Element to upload multiple files +You can upload binary files to a vault using the Skyflow File Element. Use the following steps to securely upload a file. +### Step 1: Create a container + +Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: + +```javascript +const containerOptions = { layout: [1] } + +// Creating a composable container. +const composableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); +``` + +### Step 2: Create a File Element + +Skyflow Collect Elements are defined as follows: + +```javascript +const collectElement = { + type: Skyflow.ElementType.MULTI_FILE_INPUT, // Skyflow.ElementType enum. + table: 'string', // The table this data belongs to. + column: 'string', // The column into which this data should be inserted. + inputStyles: {}, // Optional, styles that should be applied to the form element. + labelStyles: {}, // Optional, styles that will be applied to the label of the collect element. + errorTextStyles:{}, // Optional, styles that will be applied to the errorText of the collect element. +} +``` +The `table` and `column` fields indicate which table and column the Element corresponds to. + +**Notes**: +- Use period-delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`). + +### Step 3: Mount container to the DOM +Elements used for rendering files are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-dom-1). + +### Step 4: Collect data from elements + +When you're ready to upload the file, call the `uploadMultipleFiles()` method on the element. + +```javascript +const metaData = {card_number: '123'} // Optional: used to generate Skyflow IDs, and upload files to those IDs + +element.uploadMultipleFiles(); +``` +Note: +- If `MetaData` is provided, that will be used to generate Skyflow IDs, and upload files to those IDs +- If `MetaData` is not provided, the files will be uploaded as a new record. + +### File upload limitations: + +- Only non-executable file are allowed to be uploaded. +- Files have a default maximum size of 32 MB per file. This limit is configurable using the `maxFileSize` option. +- Up to 4 files can be uploaded at a time by default. This limit is configurable using the `maxFileCount` option. +- File columns can't enable tokenization, redaction, or arrays. +- Re-uploading a file overwrites previously uploaded data. +- Partial uploads or resuming a previous upload isn't supported. + +### End-to-end file upload + +```javascript +// Step 1. +const containerOptions = { layout: [1] } + +// Creating a composable container. +const container = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Step 2. +const element = container.create({ + table: 'pii_fields', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.MULTI_FILE_INPUT, +}); + +// Step 3. +container.mount('#file'); // Assumes there is a div with id='#file' in the webpage. + +// Step 4. +element.uploadMultipleFiles(); +``` + +**Sample Response :** +```javascript +{ + fileUploadResponse: [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` +### File upload with options: + +Along with fileElementInput, you can define other options in the Options object as described below: +```js +const options = { + allowedFileType: String[], // Optional. Restricts uploads to the listed file extensions (e.g. [".pdf", ".png"]). + blockEmptyFiles: Boolean, // Optional. When true, rejects files with 0 bytes. Default: false. + preserveFileName: Boolean, // Optional. When true, keeps the original filename on upload. Default: false. + maxFileSize: Number, // Optional. Maximum size in bytes for each individual file. Default: 32000000 (32 MB). + maxFileCount: Number, // Optional. Maximum number of files that can be selected at once. Must be a positive integer. Default: 4. +} +``` + +- `allowedFileType`: An array of strings indicating which file extensions are accepted for upload. +- `blockEmptyFiles`: When `true`, files with a size of 0 bytes are rejected. +- `preserveFileName`: When `true`, the original filename is preserved on upload. +- `maxFileSize`: Maximum allowed size **per file**, in bytes. If any file exceeds this limit, a validation error is shown with the filename. Defaults to `32000000` (32 MB). Only applies to `MULTI_FILE_INPUT` elements. +- `maxFileCount`: Maximum number of files that can be selected for a single upload. Must be a positive integer. Defaults to `4`. Only applies to `MULTI_FILE_INPUT` elements. + +#### File upload with options example + +```javascript +// Create collect Container. +const containerOptions = { layout: [1] } + +// Creating a composable container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Create collect elements. +const cardNumberElement = collectContainer.create({ + table: 'newTable', + column: 'card_number', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); +const options = { + allowedFileType: [".pdf", ".png"], + maxFileSize: 5000000, // 5 MB per file + maxFileCount: 3, // up to 3 files at once +}; +const fileElement = collectContainer.create({ + table: 'newTable', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.MULTI_FILE_INPUT, +}, + options +); + +// Mount the elements. +collectContainer.mount('#collectContainer'); + +// Collect and upload methods. +collectContainer.collect({}); +fileElement.uploadMultipleFiles(); + +``` +**Sample Response for collect():** +```javascript +{ + "records": [ + { + "table": "newTable", + "fields": { + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + } + } + ] +} +``` +**Sample Response for file uploadFiles() :** +```javascript +{ + "fileUploadResponse": [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + } + ] +} +``` +#### File upload with additional elements + +```javascript +// Create collect Container. +const containerOptions = { layout: [1,1] } + +// Creating a composable container. +const collectContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSABLE, containerOptions); + +// Create collect elements. +const cardNumberElement = collectContainer.create({ + table: 'newTable', + column: 'card_number', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + placeholder: 'card number', + label: 'Card Number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +const fileElement = collectContainer.create({ + table: 'newTable', + column: 'file', + skyflowID: '431eaa6c-5c15-4513-aa15-29f50babe882', + inputstyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + type: Skyflow.ElementType.MULTI_FILE_INPUT, +}); + +// Mount the elements. +collectContainer.mount('#collectContainer'); + +// Collect and upload methods. +collectContainer.collect({}); +fileElement.uploadMultipleFiles(); + +``` +**Sample Response for collect():** +```javascript +{ + "records": [ + { + "table": "newTable", + "fields": { + "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + } + } + ] +} +``` +**Sample Response for file uploadFiles() :** +```javascript +{ + "fileUploadResponse": [ + { + "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882" + }, + { + "skyflow_id": "546eaa6c-5c15-4513-aa15-29f50babe809" + } + ] +} +``` +Note: File name should contain only alphanumeric characters and !-_.*() + +--- + + +# Securely revealing data client-side +- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) +- [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) +- [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) +- [**Set token for Reveal Elements**](#set-token-for-reveal-elements) +- [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) +- [**Render a file with a File Element**](#render-a-file-with-a-file-element) +- [**Update Reveal Elements**](#update-reveal-elements) +- [**Using Composable Reveal Elements to reveal data**](#using-composable-reveal-elements-to-reveal-data) +- [**Update Composable Reveal Elements**](#update-reveal-composable-elements) +- [**Render a file with a composable file element**](#render-a-file-with-a-composable-file-element) + + +## Retrieving data from the vault + +For non-PCI use-cases, retrieving data from the vault and revealing it in the browser can be done either using the SkyflowID's, unique column values or tokens as described below + +- ### Using Skyflow tokens + In order to retrieve data from your vault using tokens that you have previously generated for that data, you can use the `detokenize(records)` method. The records parameter takes a JSON object that contains `records` to be fetched as shown below. + +```javascript +const records = { + records: [ + { + token: 'string', // Token for the record to be fetched. + redaction: RedactionType // Optional. Redaction to be applied for retrieved data. + }, + ], +}; + +Note: If you do not provide a redaction type, RedactionType.PLAIN_TEXT is the default. + +skyflow.detokenize(records); +``` +An [example](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/pure-js.html) of a detokenize call: + +```javascript +skyflow.detokenize({ + records: [ + { + token: '131e70dc-6f76-4319-bdd3-96281e051051', + }, + { + token: '1r434532-6f76-4319-bdd3-96281e051051', + redaction: Skyflow.RedactionType.MASKED + } + ], +}); +``` + +The sample response: +```javascript +{ + "records": [ + { + "token": "131e70dc-6f76-4319-bdd3-96281e051051", + "value": "1990-01-01", + "valueType": "STRING" + }, + { + "token": "1r434532-6f76-4319-bdd3-96281e051051", + "value": "xxxxxxer", + "valueType": "STRING" + } + ] +} +``` + +- ### Using Skyflow ID's or Unique Column Values + You can retrieve data from the vault with the `get(records, options)` method using either Skyflow IDs or unique column values. + + The records parameter accepts a JSON object that contains an array of either Skyflow IDs or unique column names and values. + + The options is an optional `IGetOptions` object that retrieves the tokens for SkyflowIDs. + + Notes: + + - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. + - `options` parameter is applicable only for retrieving tokens using Skyflow ID. + - You can't pass options along with the redaction type. + - `tokens` defaults to false. + + Skyflow.RedactionTypes accepts four values: + - `PLAIN_TEXT` + - `MASKED` + - `REDACTED` + - `DEFAULT` + + You must apply a redaction type to retrieve data. + +#### Schema (Skyflow IDs) + +```javascript +data = { + records: [ + { + ids: ["SKYFLOW_ID_1", "SKYFLOW_ID_2"], // List of skyflow_ids for the records to fetch. + table: "NAME_OF_SKYFLOW_TABLE", // Name of table holding the records in the vault. + redaction: Skyflow.RedactionType, // Redaction type to apply to retrieved data. + }, + ], +}; +``` +#### Schema (Unique column values) + +```javascript +data = { + records: [ + { + table: "NAME_OF_SKYFLOW_TABLE", // Name of table holding the records in the vault. + columnName: "UNIQUE_COLUMN_NAME", // Unique column name in the vault. + columnValues: [ // List of given unique column values. + "", + "", + ], // Required when specifying a unique column + redaction: Skyflow.RedactionType, // Redaction type applies to retrieved data. + + }, + ], +}; +``` +[Example usage (Skyflow IDs)](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/get-pure-js.html) + +```javascript +skyflow.get({ + records: [ + { + ids: ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9"], + table: "cards", + redaction: Skyflow.RedactionType.PLAIN_TEXT, + }, + { + ids: ["da26de53-95d5-4bdb-99db-8d8c66a35ff9"], + table: "contacts", + redaction: Skyflow.RedactionType.PLAIN_TEXT, + }, + ], +}); +``` +Example response + +```javascript +{ + "records": [ + { + "fields": { + "card_number": "4111111111111111", + "cvv": "127", + "expiry_date": "11/2035", + "fullname": "myname", + "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" + }, + "table": "cards" + } + ], + "errors": [ + { + "error": { + "code": "404", + "description": "No Records Found" + }, + "ids": ["da26de53-95d5-4bdb-99db-8d8c66a35ff9"] + } + ] +} +``` +[Example usage (Unique column values)](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/get-pure-js.html) + +```javascript +skyflow.get({ + records: [ + { + table: "cards", + redaction: RedactionType.PLAIN_TEXT, + columnName: "card_id", + columnValues: ["123", "456"], + } + ], +}); +``` +Sample response: +```javascript +{ + "records": [ + { + "fields": { + "card_id": "123", + "expiry_date": "11/35", + "fullname": "myname", + "id": "f8d2-b557-4c6b-a12c-c5ebfd9" + }, + "table": "cards" + }, + { + "fields": { + "card_id": "456", + "expiry_date": "10/23", + "fullname": "sam", + "id": "da53-95d5-4bdb-99db-8d8c5ff9" + }, + "table": "cards" + } + ] +} +``` + +[Example usage (Fetch tokens using Skyflow IDs)](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/get-pure-js.html) +```javascript +skyflow.get({ + records: [ + { + ids: [ + "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9", + "da26de53-95d5-4bdb-99db-8d8c66a35ff9" + ], + table: "cards", + }, + ], +}, { tokens: true }); +``` +Sample response: +```javascript +{ + "records": [ + { + "fields": { + "card_id": "f689e421-4cf8-4438-8dbd-cc8e7654b7d9", + "expiry_date": "d9ef1cb8-5c22-48b0-b769-64ac20ccee01", + "fullname": "37480f82-d237-4efc-a06a-ebe57121be06", + "id": "f8d2-b557-4c6b-a12c-c5ebfd9" + }, + "table": "cards" + }, + { + "fields": { + "card_id": "d794b64c-e283-4fb8-8eef-9f6710730b69", + "expiry_date": "ff848fc3-a093-4ed4-9414-877b74a33111", + "fullname": "dfb6c247-3ee6-4fd2-8d1e-19d8e11c25ce", + "id": "da53-95d5-4bdb-99db-8d8c5ff9" + }, + "table": "cards" + } + ] +} +``` + +## Using Skyflow Elements to reveal data + +Skyflow Elements can be used to securely reveal data in a browser without exposing your front end to the sensitive data. This is great for use cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. + +### Step 1: Create a container +To start, create a container using the `container(Skyflow.ContainerType)` method of the Skyflow client as shown below. + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL) +``` + +### Step 2: Create a reveal Element + +Then define a Skyflow Element to reveal data as shown below. + +```javascript +const revealElement = { + token: 'string', // Required, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. + redaction: RedactionType, //Optional, Redaction Type to be applied to data, RedactionType.PLAIN_TEXT will be applied if not provided. +}; +``` + +Note: If you don't provide a redaction type, RedactionType.PLAIN_TEXT will apply by default. + +The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data. But for reveal element, `inputStyles` accepts only `base` variant, `copyIcon` and `global` style objects. + +An example of a inputStyles object: + +```javascript +inputStyles: { + base: { + color: '#1d1d1d', + }, + copyIcon: { + position: 'absolute', + right: '8px', + top: 'calc(50% - 10px)', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a labelStyles object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a errorTextStyles object: + +```javascript +errorTextStyles: { + base: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +Along with RevealElementInput, you can define other options in the RevealElementOptions object as described below: +```js +const options = { + enableCopy: false, // Optional, enables the copy icon to reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {} // Optional, indicates the allowed data type value for format. +} +``` + +`format`: A string value that indicates how the reveal element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified to any character in the `format` value is considered as a string literal. + +`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `{ ‘X’: ‘[0-9]’ }`. + +**Reveal Element Options examples:** +Example 1 +```js +const revealElementInput = { + token: '' +}; + +const options = { + format: '(XXX) XXX-XXXX', + translation: { 'X': '[0-9]'} +}; + +const revealElement = revealContainer.create(revealElementInput,options); +``` + +Value from vault: "1234121234" +Revealed Value displayed in element: "(123) 412-1234" + +Example 2: +```js +const revealElementInput = { + token: '' +}; + +const options = { + format: 'XXXX-XXXXXX-XXXXX', + translation: { 'X': '[0-9]' } +}; + +const revealElement = revealContainer.create(revealElementInput,options); +``` + +Value from vault: "374200000000004" +Revealed Value displayed in element: "3742-000000-00004" + +Once you've defined a Skyflow Element, you can use the `create(element)` method of the container to create the Element as shown below: + +```javascript +const element = container.create(revealElement) +``` + +### Step 3: Mount Elements to the DOM + +Elements used for revealing data are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-dom). + + +### Step 4: Reveal data +When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: + +```javascript +container + .reveal() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + + +### End to end example of all steps + +**[Sample Code:](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/skyflow-elements.html)** +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +// Step 2. +const cardNumberElement = container.create({ + token: 'b63ec4e0-bbad-4e43-96e6-6bd50f483f75', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + label: 'card_number', + altText: 'XXXX XXXX XXXX XXXX', + redaction: Skyflow.RedactionType.MASKED +}); + +const cvvElement = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'cvv', + altText: 'XXX', +}); + +const expiryDate= container.create({ + token: 'a4b24714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'expiryDate', + altText: 'MM/YYYY', +}); +// Step 3. +cardNumberElement.mount('#cardNumber'); // Assumes there is a placeholder div with id='cardNumber' on the page +cvvElement.mount('#cvv'); // Assumes there is a placeholder div with id='cvv' on the page +expiryDate.mount('#expiryDate'); // Assumes there is a placeholder div with id='expiryDate' on the page + +// Step 4. +container + .reveal() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + +The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. + +### Sample Response + +``` +{ + "success": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "value": "xxxxxxxxx4163" + "valueType": "STRING" + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "value": "12/2098" + "valueType": "STRING" + } + ], + "errors": [ + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "error": { + "code": 404, + "description": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047" + } + } + ] +} +``` + +### UI Error for Reveal Elements +Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. + +`setError(error: string)` method is used to set the error text for the element, when this method is triggered, all the current errors present on the element will be overridden with the custom error message passed. This error will be displayed on the element until `resetError()` is triggered on the same element. + +`resetError()` method is used to clear the custom error message that is set using `setError`. + +##### Sample code snippet for setError and resetError + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', +}); + +// Set custom error. +cardNumber.setError('custom error'); + +// Reset custom error. +cardNumber.resetError(); +``` + +### Override default error messages + +You can override the default error messages with custom ones by using `setErrorOverride`. This is especially useful to override default error messages in non-English languages. + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', +}); + +const revealButton = document.getElementById('revealPCIData'); + +if (revealButton) { + revealButton.addEventListener('click', () => { + revealContainer.reveal().then((res) => { + //handle reveal response + }).catch((err) => { + cardNumber.setErrorOverride("custom error") + }); + }); +} +``` + +### Set token for Reveal Elements + +The `setToken(value: string)` method can be used to set the token of the Reveal Element. If no altText is set, the set token will be displayed on the UI as well. If altText is set, then there will be no change in the UI but the token of the element will be internally updated. + +##### Sample code snippet for setToken +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + altText: 'Card Number', +}); + +// Set token. +cardNumber.setToken('89024714-6a26-4256-b9d4-55ad69aa4047'); +``` +### Set and Clear altText for Reveal Elements +The `setAltText(value: string)` method can be used to set the altText of the Reveal Element. This will cause the altText to be displayed in the UI regardless of whether the token or value is currently being displayed. + +`clearAltText()` method can be used to clear the altText, this will cause the element to display the token or actual value of the element. If the element has no token, the element will be empty. +##### Sample code snippet for setAltText and clearAltText + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const cardNumber = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', +}); + +// Set altText. +cardNumber.setAltText('Card Number'); + +// Clear altText. +cardNumber.clearAltText(); + +``` + +## Render a file with a File Element + +You can render files using the Skyflow File Element. Use the following steps to securely render a file. + +### Step 1: Create a container +Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL) +``` + +### Step 2: Create a File Element +Define a Skyflow Element to render the file as shown below. + +```javascript +const fileElement = { + inputStyles: {}, // Optional, styles to be applied to the element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the render element. + altText: 'string', // Optional, string that is shown before file render call + skyflowID: 'string', // Required, skyflow id of the file to render + column: 'string', // Required, column name of the file to render + table: 'string', // Required, table name of the file to render +}; +``` +The inputStyles and errorTextStyles parameters accept a styles object as described in the [previous section](https://github.com/skyflowapi/skyflow-js#step-2-create-a-collect-element) for collecting data. But for render file elements, inputStyles accepts only base variant, global style objects. + +An example of a inputStyles object: + +```javascript +inputStyles: { + base: { + height: '400px', + width: '300px', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` +An example of a errorTextStyles object: +```javascript +errorTextStyles: { + base: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` + +### Step 3: Mount Elements to the DOM +Elements used for rendering files are mounted to the DOM the same way as Elements used for collecting data. Refer to Step 3 of the [section above](https://github.com/skyflowapi/skyflow-js#step-3-mount-elements-to-the-dom). + +### Step 4: Render File +After you create and mount the element, call the `renderFile()` method on the element as shown below: +```javascript +fileElement + .renderFile() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + +### End to end example of file render +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +// REPLACE with your custom implementation to fetch skyflow_id from backend service. +// Sample implementation +fetch("") + .then((response) => { + + // on successful fetch skyflow_id + const skyflowID = response.skyflow_id; + + // Step 2. + const fileElement = container.create({ + skyflowID: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + column: "file", + table: "table", + inputStyles: { + base: { + height: "400px", + width: "300px", + }, + }, + errorTextStyles: { + base: { + color: "#f44336", + }, + }, + altText: "This is an altText", + }); + // Step 3. + fileElement.mount("#renderFile"); // Assumes there is a placeholder div with id=renderFile on the page + + const renderButton = document.getElementById("renderFiles"); // button to call render file + + if (renderButton) { + renderButton.addEventListener("click", () => { + + // Step 4. + fileElement + .renderFile() + .then((data) => { + // Handle success. + }) + .catch((err) => { + // Handle error. + }); + }); + } + }) + .catch((err) => { + // failed to fetch skyflow_id + console.log(err); + }); + +``` + +### Sample Success Response +```json +{ + "success": [ + { + "skyflow_id": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "column": "file" + }, + ] +} +``` + +## Update Reveal Elements + +You can update reveal element properties with the `update` interface. + +The `update` interface takes the below object: +```javascript +const updateElement = { + token: 'string', // Optional, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. + redaction: RedactionType, // Optional, Redaction Type to be applied to data. + skyflowID: 'string', // Optional, Skyflow ID of the file to render. + table: 'string', // Optional, table name of the file to render. + column: 'string' // Optional, column name of the file to render. +}; +``` + +Only include the properties that you want to update for the specified reveal element. + +Properties your provided when you created the element remain the same until you explicitly update them. + +### End to end example +```javascript +// Create a reveal container. +const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: { + color: '#f44336' + }, + }, +}; + +// Create reveal elements +const cardHolderNameRevealElement = revealContainer.create({ + token: 'ed5fdd1f-5009-435c-a06b-3417ce76d2c8', + altText: 'first name', + ...stylesOptions, + label: 'Card Holder Name', +}); + +const cardNumberRevealElement = revealContainer.create({ + token: '8ee84061-7107-4faf-bb25-e044f3d191fe', + altText: 'xxxx', + ...stylesOptions, + label: 'Card Number', + redaction: 'RedactionType.CARD_NUMBER' +}); + +// Mount the reveal elements. +cardHolderNameRevealElement.mount('#cardHolderNameRevealElement'); // Assumes there is a div with id='#cardHolderNameRevealElement' in the webpage. +cardNumberRevealElement.mount('#cardNumberRevealElement'); // Assumes there is a div with id='#cardNumberRevealElement' in the webpage. + +// ... + +// Update label, labelStyles properties on cardHolderNameRevealElement. +cardHolderNameRevealElement.update({ + label: 'CARDHOLDER NAME', + labelStyles: { + base: { + color: '#aa11aa' + } + } +}); + +// Update inputStyles, errorTextStyles properties on cardNumberRevealElement. +cardNumberRevealElement.update({ + inputStyles: { + base: { + color: '#fff', + backgroundColor: '#000', + borderColor: '#f00', + borderWidth: '5px' + } + }, + errorTextStyles: { + base: { + backgroundColor: '#000', + } + } +}); +``` + +--- + + +## Using Composable Reveal Elements to reveal data + +Composable Reveal Elements combine multiple Skyflow Elements in a single iframe, letting you create multiple Skyflow Elements in a single row. The following steps create a composable reveal element and securely collect data through it. + +### Step 1: Create a composable reveal container + +Create a container for the composable reveal element using the `container(Skyflow.ContainerType)` method of the Skyflow client: + +``` javascript + const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); +``` +Pass an options object that contains the following keys: + +1. `layout`: An array that indicates the number of rows in the container and the number of elements in each row. The index value of the array defines the number of rows, and each value in the array represents the number of elements in that row, in order. + + For example: `[2,1]` means the container has two rows, with two elements in the first row and one element in the second row. + + `Note`: The sum of values in the layout array should be equal to the number of elements created + +2. `styles`: CSS styles to apply to the reveal composable container. +3. `errorTextStyles`: CSS styles to apply if an error is encountered. + +```javascript +const containerOptions = { + layout: [2, 1], // Required + styles: { // Optional + base: { + border: '1px solid #DFE3EB', + padding: '8px', + borderRadius: '4px', + margin: '12px 2px', + }, + }, + errorTextStyles: { // Optional + base: { + color: 'red', + fontFamily: '"Roboto", sans-serif' + }, + global: { + '@import': 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } + }, +}; +``` + +### Step 2: Create Composable Reveal Elements +Composable Reveal Elements use the following schema: + +```javascript +const revealComposableElement = { + token: 'string', // Required, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. + redaction: RedactionType, //Optional, Redaction Type to be applied to data, RedactionType.PLAIN_TEXT will be applied if not provided. +}; +``` +Note: If you don't provide a redaction type, RedactionType.PLAIN_TEXT will apply by default. + +The `inputStyles`, `labelStyles` and `errorTextStyles` parameters accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data. But for reveal element, `inputStyles` accepts only `base` variant, `copyIcon` and `global` style objects. + +An example of a inputStyles object: + +```javascript +inputStyles: { + base: { + color: '#1d1d1d', + }, + copyIcon: { + position: 'absolute', + right: '8px', + top: 'calc(50% - 10px)', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a labelStyles object: + +```javascript +labelStyles: { + base: { + fontSize: '12px', + fontWeight: 'bold', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +An example of a errorTextStyles object: + +```javascript +errorTextStyles: { + base: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +}, +``` + +Along with RevealElementInput, you can define other options in the RevealElementOptions object as described below: +```js +const options = { + enableCopy: false, // Optional, enables the copy icon to reveal elements to copy text to clipboard. Defaults to 'false'). + format: String, // Optional, format for the element + translation: {} // Optional, indicates the allowed data type value for format. +} +``` + +`format`: A string value that indicates how the reveal element should display the value, including placeholder characters that map to keys `translation` If `translation` isn't specified to any character in the `format` value is considered as a string literal. + +`translation`: An object of key value pairs, where the key is a character that appears in `format` and the value is a simple regex pattern of acceptable inputs for that character. Each key can only appear once. Defaults to `{ ‘X’: ‘[0-9]’ }`. + +**Reveal Element Options examples:** +Example 1 +```js +const revealElementInput = { + token: '' +}; + +const options = { + format: '(XXX) XXX-XXXX', + translation: { 'X': '[0-9]'} +}; + +const revealElement = revealComposableContainer.create(revealElementInput,options); +``` + +Value from vault: "1234121234" +Revealed Value displayed in element: "(123) 412-1234" + +Example 2: +```js +const revealElementInput = { + token: '' +}; + +const options = { + format: 'XXXX-XXXXXX-XXXXX', + translation: { 'X': '[0-9]' } +}; + +const revealElement = revealComposableContainer.create(revealElementInput,options); +``` + +Value from vault: "374200000000004" +Revealed Value displayed in element: "3742-000000-00004" + +Once you've defined a Skyflow Element, you can use the `create(element)` method of the container to create the Element as shown below: + +```javascript +const element = revealComposableContainer.create(revealElement) +``` + +### Step 3: Mount Container to the DOM +To specify where the Elements are rendered on your page, create a placeholder `
    ` element with unique `id` attribute. Use this empty `
    ` placeholder to mount the composable reveal container. + +```javascript +
    +
    +
    +
    + + +``` +Use the composable container's `mount(domElement)` method to insert the container's Elements into the specified `
    `. For instance, the following call inserts Elements into the `
    ` with the `id "#composableContainer"`. + +```javacript +revealComposableContainer.mount('#composableRevealContainer'); +``` + +### Step 4: Reveal data +When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: + +```javascript +container + .reveal() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + +### End to end example of reveal data with Composable Reveal Elements +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); +// Step 2. +const cardNumberElement = container.create({ + token: 'b63ec4e0-bbad-4e43-96e6-6bd50f483f75', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + labelStyles: { + base: { + fontSize: '12px', + }, + }, + errorTextStyles: { + base: { + color: '#f44336', + }, + }, + label: 'card_number', + altText: 'XXXX XXXX XXXX XXXX', + redaction: Skyflow.RedactionType.MASKED +}); + +const cvvElement = container.create({ + token: '89024714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'cvv', + altText: 'XXX', +}); + +const expiryDate= container.create({ + token: 'a4b24714-6a26-4256-b9d4-55ad69aa4047', + inputStyles: { + base: { + color: '#1d1d1d', + }, + }, + label: 'expiryDate', + altText: 'MM/YYYY', +}); +// Step 3. +container.mount('#container') +// Step 4. +container + .reveal() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` +The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. + +### Sample Response + +``` +{ + "success": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "value": "xxxxxxxxx4163" + "valueType": "STRING" + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "value": "12/2098" + "valueType": "STRING" + } + ], + "errors": [ + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "error": { + "code": 404, + "description": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047" + } + } + ] +} +``` + +## Update Reveal Composable Elements + +You can update reveal composable element properties with the `update` interface. + +The `update` interface takes the below object: +```javascript +const updateElement = { + token: 'string', // Optional, token of the data being revealed. + inputStyles: {}, // Optional, styles to be applied to the element. + labelStyles: {}, // Optional, styles to be applied to the label of the reveal element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the reveal element. + label: 'string', // Optional, label for the form element. + altText: 'string', // Optional, string that is shown before reveal, will show token if altText is not provided. + redaction: RedactionType, // Optional, Redaction Type to be applied to data. + skyflowID: 'string', // Optional, Skyflow ID of the file to render. + table: 'string', // Optional, table name of the file to render. + column: 'string' // Optional, column name of the file to render. +}; +``` + +Only include the properties that you want to update for the specified reveal element. + +Properties your provided when you created the element remain the same until you explicitly update them. + + +### End to end example +```javascript +// Create a reveal composable container. +const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + +const stylesOptions = { + inputStyles: { + base: { + fontFamily: 'Inter', + fontStyle: 'normal', + fontWeight: 400, + fontSize: '14px', + lineHeight: '21px', + width: '294px', + }, + }, + labelStyles: {}, + errorTextStyles: { + base: { + color: '#f44336' + }, + }, +}; + +// Create reveal elements +const cardHolderNameRevealElement = revealComposableContainer.create({ + token: 'ed5fdd1f-5009-435c-a06b-3417ce76d2c8', + altText: 'first name', + ...stylesOptions, + label: 'Card Holder Name', +}); + +const cardNumberRevealElement = revealComposableContainer.create({ + token: '8ee84061-7107-4faf-bb25-e044f3d191fe', + altText: 'xxxx', + ...stylesOptions, + label: 'Card Number', + redaction: 'RedactionType.CARD_NUMBER' +}); + +// Mount the reveal elements. +revealContainer.mount('#container'); // Assumes there is a div with container +// ... + +// Update label, labelStyles properties on cardHolderNameRevealElement. +cardHolderNameRevealElement.update({ + label: 'CARDHOLDER NAME', + labelStyles: { + base: { + color: '#aa11aa' + } + } +}); + +// Update inputStyles, errorTextStyles properties on cardNumberRevealElement. +cardNumberRevealElement.update({ + inputStyles: { + base: { + color: '#fff', + backgroundColor: '#000', + borderColor: '#f00', + borderWidth: '5px' + } + }, + errorTextStyles: { + base: { + backgroundColor: '#000', + } + } +}); +``` + +--- + + +## Render a file with a Composable File Element + +You can render files using the Skyflow File Element. Use the following steps to securely render a file. + +### Step 1: Create a container +Create a container for the form elements using the container(Skyflow.ContainerType) method of the Skyflow client: + +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions) +``` + +### Step 2: Create a File Element +Define a Skyflow Element to render the file as shown below. + +```javascript +const fileElement = { + inputStyles: {}, // Optional, styles to be applied to the element. + errorTextStyles: {}, // Optional, styles that will be applied to the errorText of the render element. + altText: 'string', // Optional, string that is shown before file render call + skyflowID: 'string', // Required, skyflow id of the file to render + column: 'string', // Required, column name of the file to render + table: 'string', // Required, table name of the file to render +}; +``` +The inputStyles and errorTextStyles parameters accept a styles object as described in the [previous section](https://github.com/skyflowapi/skyflow-js#step-2-create-a-collect-element) for collecting data. But for render file elements, inputStyles accepts only base variant, global style objects. + +An example of a inputStyles object: + +```javascript +inputStyles: { + base: { + height: '400px', + width: '300px', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` +An example of a errorTextStyles object: +```javascript +errorTextStyles: { + base: { + color: '#f44336', + }, + global: { + '@import' :'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + } +} +``` +### Step 3: Mount Container to the DOM +Mount Elements for file rendering to the DOM the same way as Elements used for revealing data. Refer to Step 3 of the [section above](#step-3-mount-container-to-the-dom). + +### Step 4: Render File +After you create and mount the element, call the renderFile() method on the element as shown below: +```javascript +fileElement + .renderFile() + .then(data => { + // Handle success. + }) + .catch(err => { + // Handle error. + }); +``` + +### End to end example of file render +```javascript +// Step 1. +const container = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + +// REPLACE with your custom implementation to fetch skyflow_id from backend service. +// Sample implementation +fetch("") + .then((response) => { + + // on successful fetch skyflow_id + const skyflowID = response.skyflow_id; + + // Step 2. + const fileElement = container.create({ + skyflowID: "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + column: "file", + table: "table", + inputStyles: { + base: { + height: "400px", + width: "300px", + }, + }, + errorTextStyles: { + base: { + color: "#f44336", + }, + }, + altText: "This is an altText", + }); + // Step 3. + fileElement.mount("#renderFile"); // Assumes there is a placeholder div with id=renderFile on the page + + const renderButton = document.getElementById("renderFiles"); // button to call render file + + if (renderButton) { + renderButton.addEventListener("click", () => { + + // Step 4. + fileElement + .renderFile() + .then((data) => { + // Handle success. + }) + .catch((err) => { + // Handle error. + }); + }); + } + }) + .catch((err) => { + // failed to fetch skyflow_id + console.log(err); + }); + +``` + +# Securely deleting data client-side +- [**Deleting data from the vault**](#deleting-data-from-the-vault) + +## Deleting data from the vault + +To delete data from the vault, use the `delete(records, options?)` method of the Skyflow client. The `records` parameter takes an array of records to delete in the following format. The `options` parameter is optional and takes an object of deletion parameters. Currently, there are no supported deletion parameters. + +```javascript +const records = [ + { + id: "", // skyflow id of the record to delete + table: "" // Table from which the record is to be deleted + }, + { + // ...additional records here + }, +], + +skyflowClient.delete(records); +``` + +An [example](https://github.com/skyflowapi/skyflow-js/blob/main/packages/skyflow-js/samples/using-script-tag/delete-pure-js.html) of delete call: + +```javascript +skyflowClient.delete({ + records: [ + { + id: "29ebda8d-5272-4063-af58-15cc674e332b", + table: "cards", + }, + { + id: "d5f4b926-7b1a-41df-8fac-7950d2cbd923", + table: "cards", + } + ], +}); +``` + +A sample response: + +```json +{ + "records": [ + { + "skyflow_id": "29ebda8d-5272-4063-af58-15cc674e332b", + "deleted": true, + }, + { + "skyflow_id": "29ebda8d-5272-4063-af58-15cc674e332b", + "deleted": true, + } + ] +} +``` + +# Set Custom Network messages on container: + +Add custom network error messages to a container with the `setError` method. + +`setError(ErrorMessages: Record)` sets the error text for the different network errors types. When this method is triggered, all the errors present in the error response are overridden with the specified custom error message. This error is sent on the collect or upload file call on the same container. + +### Sample code snippet for setError on collect container +```javascript +const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); + +const cardNumber = container.create({ + table: 'pii_fields', + column: 'primary_card.card_number', + type: Skyflow.ElementType.CARD_NUMBER, +}); + +// Set custom error. +container.setError({ + [Skyflow.ErrorType.BAD_REQUEST]: "Bad request. Please check the request payload.", + [Skyflow.ErrorType.UNAUTHORIZED]: "You are not authorized. Please check your token.", + [Skyflow.ErrorType.FORBIDDEN]: "Access denied. You do not have permission to perform this action.", + [Skyflow.ErrorType.TOO_MANY_REQUESTS]: "Too many requests. Please try again later.", + [Skyflow.ErrorType.INTERNAL_SERVER_ERROR]: "Something went wrong on our end. Please try again later.", + [Skyflow.ErrorType.BAD_GATEWAY]: "Received an invalid response from the server. Please try again.", + [Skyflow.ErrorType.SERVICE_UNAVAILABLE]: "Service is temporarily unavailable. Please try again later.", + [Skyflow.ErrorType.CONNECTION]: "Unable to connect to the server. Please check your network connection.", + [Skyflow.ErrorType.NOT_FOUND]: "Table not found with custom message", + [Skyflow.ErrorType.OFFLINE]: "You appear to be offline. Please check your internet connection.", + [Skyflow.ErrorType.TIMEOUT]: "The request took too long to respond. Please try again.", + [Skyflow.ErrorType.ABORT]: "The request was aborted.", + [Skyflow.ErrorType.NETWORK_GENERIC]: "A network error occurred. Please try again.", +}); + +container + .collect() + .then(res => console.log(res)) + .catch(err =>{ + console.log(err); +}) +``` +#### Sample Error structure: +```json +{ + "error":{ + "code":0, + "description":"You appear to be offline. Please check your internet connection.", + "type":"OFFLINE" + }, +} +``` + +`Skyflow.ErrorType` accepts following values: + - `BAD_REQUEST` + - `UNAUTHORIZED` + - `FORBIDDEN` + - `TOO_MANY_REQUESTS` + - `INTERNAL_SERVER_ERROR` + - `BAD_GATEWAY` + - `SERVICE_UNAVAILABLE` + - `CONNECTION` + - `NOT_FOUND` + - `OFFLINE` + - `TIMEOUT` + - `NETWORK_GENERIC` + - `ABORT` + + +## Reporting a Vulnerability + +If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. + + diff --git a/packages/skyflow-js/jest.config.json b/packages/skyflow-js/jest.config.json new file mode 100644 index 000000000..7ef2f9493 --- /dev/null +++ b/packages/skyflow-js/jest.config.json @@ -0,0 +1,23 @@ +{ + "verbose": true, + "collectCoverage": true, + "collectCoverageFrom": [ + "/src/**/*.{ts,tsx}", + "/../../core/**/*.{ts,tsx}", + "!**/*.d.ts", + "!/src/index.ts", + "!/src/index-node.ts", + "!/src/index-internal.ts" + ], + "testEnvironment": "jsdom", + "testTimeout": 30000, + "setupFiles": ["/tests/jest.setup.js"], + "moduleNameMapper":{ + "^@core/(.*)$": "/../../core/$1", + "^.+\\.svg$": "/tests/__mocks__/file-mock.js" + }, + "transform": { + "^.+\\.[jt]sx?$": ["babel-jest", { "rootMode": "upward" }] + }, + "transformIgnorePatterns": ["/node_modules/(?!mime)"] +} diff --git a/packages/skyflow-js/package.json b/packages/skyflow-js/package.json new file mode 100644 index 000000000..050f5699a --- /dev/null +++ b/packages/skyflow-js/package.json @@ -0,0 +1,53 @@ +{ + "name": "skyflow-js", + "preferGlobal": true, + "analyze": false, + "version": "2.7.9", + "author": "Skyflow", + "description": "Skyflow JavaScript SDK", + "homepage": "https://github.com/skyflowapi/skyflow-js", + "main": "./dist/sdkNodeBuild/index.js", + "types": "./types/packages/skyflow-js/src/index-node.d.ts", + "files": [ + "dist/sdkNodeBuild", + "types" + ], + "license": "MIT", + "keywords": [ + "client", + "sdk", + "javascript" + ], + "scripts": { + "type-check": "tsc --noEmit", + "type-check:watch": "npm run type-check -- --watch", + "build:types": "tsc --emitDeclarationOnly && tsc-alias -p tsconfig.json", + "dev": "webpack serve --config=webpack.dev.js --open --hot ", + "build-browser-sdk": "webpack --config=webpack.skyflow-browser.js", + "build-node-sdk": "webpack --config=webpack.skyflow-node.js", + "build-iframe": "webpack --config=webpack.iframe.js", + "test": "jest --config=jest.config.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/skyflowapi/skyflow-js.git" + }, + "dependencies": { + "core-js": "3.44.0", + "framebus": "4.0.5", + "inject-stylesheet": "2.0.0", + "jquery": "3.7.1", + "jquery-mask-plugin": "1.14.16", + "jss": "10.10.0", + "jss-preset-default": "10.10.0", + "jwt-decode": "3.1.2", + "lodash": "4.18.1", + "mime": "3.0.0", + "regex-parser": "2.3.1", + "set-value": "4.1.0" + }, + "engines": { + "node": ">=12.0", + "npm": ">=6.0" + } +} diff --git a/samples/README.md b/packages/skyflow-js/samples/README.md similarity index 94% rename from samples/README.md rename to packages/skyflow-js/samples/README.md index c37ca4874..39dac5fc2 100644 --- a/samples/README.md +++ b/packages/skyflow-js/samples/README.md @@ -1,10 +1,16 @@ -# JS SDK samples +# skyflow-js samples + +Runnable samples for [`skyflow-js`](../README.md), Skyflow's **PDB vault** JavaScript SDK. Test the SDK by adding your `VAULT_ID`, `VAULT_URL`, and `SERVICE-ACCOUNT` details as the corresponding values in each sample. +> Working against a **Flow vault**? Use the [`skyflow-flowvault-js` samples](../../skyflow-flowvault-js/samples/README.md) instead. + +Samples come in three flavors — [`using-script-tag/`](using-script-tag) (loads `https://js.skyflow.com/v2/index.js` and uses the `Skyflow` global), [`using-npm/`](using-npm) (JavaScript), and [`using-typescript/`](using-typescript). ## Prerequisites - A Skyflow account. If you don't have one, register for one on the [Try Skyflow](https://skyflow.com/try-skyflow) page. +- A **PDB vault**. - [Node.js](https://nodejs.org/en/) version 10 or above - [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) version 6.x.x - [express.js](http://expressjs.com/en/starter/hello-world.html) diff --git a/samples/using-npm/3ds-helper-functions/package.json b/packages/skyflow-js/samples/using-npm/3ds-helper-functions/package.json similarity index 100% rename from samples/using-npm/3ds-helper-functions/package.json rename to packages/skyflow-js/samples/using-npm/3ds-helper-functions/package.json diff --git a/samples/using-npm/3ds-helper-functions/src/index.html b/packages/skyflow-js/samples/using-npm/3ds-helper-functions/src/index.html similarity index 100% rename from samples/using-npm/3ds-helper-functions/src/index.html rename to packages/skyflow-js/samples/using-npm/3ds-helper-functions/src/index.html diff --git a/samples/using-npm/3ds-helper-functions/src/index.js b/packages/skyflow-js/samples/using-npm/3ds-helper-functions/src/index.js similarity index 100% rename from samples/using-npm/3ds-helper-functions/src/index.js rename to packages/skyflow-js/samples/using-npm/3ds-helper-functions/src/index.js diff --git a/packages/skyflow-js/samples/using-npm/README.md b/packages/skyflow-js/samples/using-npm/README.md new file mode 100644 index 000000000..cbb8330aa --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/README.md @@ -0,0 +1,10 @@ +### Running the Samples + +install the dependencies +``` +$ npm install +``` +run the sample +``` +$ npm start +``` \ No newline at end of file diff --git a/samples/using-npm/collect-element-listeners/package.json b/packages/skyflow-js/samples/using-npm/collect-element-listeners/package.json similarity index 100% rename from samples/using-npm/collect-element-listeners/package.json rename to packages/skyflow-js/samples/using-npm/collect-element-listeners/package.json diff --git a/packages/skyflow-js/samples/using-npm/collect-element-listeners/src/index.html b/packages/skyflow-js/samples/using-npm/collect-element-listeners/src/index.html new file mode 100644 index 000000000..50f050bf6 --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/collect-element-listeners/src/index.html @@ -0,0 +1,41 @@ + + + + + + + Collect Element Listeners + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + + diff --git a/samples/using-npm/collect-element-listeners/src/index.js b/packages/skyflow-js/samples/using-npm/collect-element-listeners/src/index.js similarity index 100% rename from samples/using-npm/collect-element-listeners/src/index.js rename to packages/skyflow-js/samples/using-npm/collect-element-listeners/src/index.js diff --git a/samples/using-npm/composable-elements-update/package.json b/packages/skyflow-js/samples/using-npm/composable-elements-update/package.json similarity index 100% rename from samples/using-npm/composable-elements-update/package.json rename to packages/skyflow-js/samples/using-npm/composable-elements-update/package.json diff --git a/samples/using-npm/composable-elements-update/src/index.html b/packages/skyflow-js/samples/using-npm/composable-elements-update/src/index.html similarity index 100% rename from samples/using-npm/composable-elements-update/src/index.html rename to packages/skyflow-js/samples/using-npm/composable-elements-update/src/index.html diff --git a/samples/using-npm/composable-elements-update/src/index.js b/packages/skyflow-js/samples/using-npm/composable-elements-update/src/index.js similarity index 100% rename from samples/using-npm/composable-elements-update/src/index.js rename to packages/skyflow-js/samples/using-npm/composable-elements-update/src/index.js diff --git a/samples/using-npm/composable-elements/package.json b/packages/skyflow-js/samples/using-npm/composable-elements/package.json similarity index 100% rename from samples/using-npm/composable-elements/package.json rename to packages/skyflow-js/samples/using-npm/composable-elements/package.json diff --git a/samples/using-npm/composable-elements/src/index.html b/packages/skyflow-js/samples/using-npm/composable-elements/src/index.html similarity index 100% rename from samples/using-npm/composable-elements/src/index.html rename to packages/skyflow-js/samples/using-npm/composable-elements/src/index.html diff --git a/samples/using-npm/composable-elements/src/index.js b/packages/skyflow-js/samples/using-npm/composable-elements/src/index.js similarity index 100% rename from samples/using-npm/composable-elements/src/index.js rename to packages/skyflow-js/samples/using-npm/composable-elements/src/index.js diff --git a/samples/using-npm/custom-validations/package.json b/packages/skyflow-js/samples/using-npm/custom-validations/package.json similarity index 100% rename from samples/using-npm/custom-validations/package.json rename to packages/skyflow-js/samples/using-npm/custom-validations/package.json diff --git a/packages/skyflow-js/samples/using-npm/custom-validations/src/index.html b/packages/skyflow-js/samples/using-npm/custom-validations/src/index.html new file mode 100644 index 000000000..62dcbcd49 --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/custom-validations/src/index.html @@ -0,0 +1,34 @@ + + + + + + + Custom Validations + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    + + + + diff --git a/samples/using-npm/custom-validations/src/index.js b/packages/skyflow-js/samples/using-npm/custom-validations/src/index.js similarity index 100% rename from samples/using-npm/custom-validations/src/index.js rename to packages/skyflow-js/samples/using-npm/custom-validations/src/index.js diff --git a/samples/using-npm/file-render/package.json b/packages/skyflow-js/samples/using-npm/file-render/package.json similarity index 100% rename from samples/using-npm/file-render/package.json rename to packages/skyflow-js/samples/using-npm/file-render/package.json diff --git a/samples/using-npm/file-render/src/index.html b/packages/skyflow-js/samples/using-npm/file-render/src/index.html similarity index 100% rename from samples/using-npm/file-render/src/index.html rename to packages/skyflow-js/samples/using-npm/file-render/src/index.html diff --git a/samples/using-npm/file-render/src/index.js b/packages/skyflow-js/samples/using-npm/file-render/src/index.js similarity index 100% rename from samples/using-npm/file-render/src/index.js rename to packages/skyflow-js/samples/using-npm/file-render/src/index.js diff --git a/samples/using-npm/pure-js get/package.json b/packages/skyflow-js/samples/using-npm/pure-js get/package.json similarity index 100% rename from samples/using-npm/pure-js get/package.json rename to packages/skyflow-js/samples/using-npm/pure-js get/package.json diff --git a/samples/using-npm/pure-js get/src/index.html b/packages/skyflow-js/samples/using-npm/pure-js get/src/index.html similarity index 100% rename from samples/using-npm/pure-js get/src/index.html rename to packages/skyflow-js/samples/using-npm/pure-js get/src/index.html diff --git a/samples/using-npm/pure-js get/src/index.js b/packages/skyflow-js/samples/using-npm/pure-js get/src/index.js similarity index 100% rename from samples/using-npm/pure-js get/src/index.js rename to packages/skyflow-js/samples/using-npm/pure-js get/src/index.js diff --git a/samples/using-typescript/pure-js-delete/.gitignore b/packages/skyflow-js/samples/using-npm/pure-js-delete/.gitignore similarity index 100% rename from samples/using-typescript/pure-js-delete/.gitignore rename to packages/skyflow-js/samples/using-npm/pure-js-delete/.gitignore diff --git a/samples/using-npm/pure-js-delete/package.json b/packages/skyflow-js/samples/using-npm/pure-js-delete/package.json similarity index 100% rename from samples/using-npm/pure-js-delete/package.json rename to packages/skyflow-js/samples/using-npm/pure-js-delete/package.json diff --git a/samples/using-npm/pure-js-delete/src/index.html b/packages/skyflow-js/samples/using-npm/pure-js-delete/src/index.html similarity index 100% rename from samples/using-npm/pure-js-delete/src/index.html rename to packages/skyflow-js/samples/using-npm/pure-js-delete/src/index.html diff --git a/samples/using-npm/pure-js-delete/src/index.js b/packages/skyflow-js/samples/using-npm/pure-js-delete/src/index.js similarity index 100% rename from samples/using-npm/pure-js-delete/src/index.js rename to packages/skyflow-js/samples/using-npm/pure-js-delete/src/index.js diff --git a/samples/using-npm/pure-js-update/package.json b/packages/skyflow-js/samples/using-npm/pure-js-update/package.json similarity index 100% rename from samples/using-npm/pure-js-update/package.json rename to packages/skyflow-js/samples/using-npm/pure-js-update/package.json diff --git a/samples/using-npm/pure-js-update/src/index.html b/packages/skyflow-js/samples/using-npm/pure-js-update/src/index.html similarity index 100% rename from samples/using-npm/pure-js-update/src/index.html rename to packages/skyflow-js/samples/using-npm/pure-js-update/src/index.html diff --git a/samples/using-npm/pure-js-update/src/index.js b/packages/skyflow-js/samples/using-npm/pure-js-update/src/index.js similarity index 100% rename from samples/using-npm/pure-js-update/src/index.js rename to packages/skyflow-js/samples/using-npm/pure-js-update/src/index.js diff --git a/samples/using-npm/pure-js/package.json b/packages/skyflow-js/samples/using-npm/pure-js/package.json similarity index 100% rename from samples/using-npm/pure-js/package.json rename to packages/skyflow-js/samples/using-npm/pure-js/package.json diff --git a/samples/using-npm/pure-js/src/index.html b/packages/skyflow-js/samples/using-npm/pure-js/src/index.html similarity index 100% rename from samples/using-npm/pure-js/src/index.html rename to packages/skyflow-js/samples/using-npm/pure-js/src/index.html diff --git a/samples/using-npm/pure-js/src/index.js b/packages/skyflow-js/samples/using-npm/pure-js/src/index.js similarity index 100% rename from samples/using-npm/pure-js/src/index.js rename to packages/skyflow-js/samples/using-npm/pure-js/src/index.js diff --git a/samples/using-npm/skyflow-elements-input-formatting/package.json b/packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/package.json similarity index 100% rename from samples/using-npm/skyflow-elements-input-formatting/package.json rename to packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/package.json diff --git a/samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js b/packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js similarity index 100% rename from samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js rename to packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/collect-input-formatting.js diff --git a/packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/index.html b/packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/index.html new file mode 100644 index 000000000..394c90164 --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/index.html @@ -0,0 +1,52 @@ + + + + + + + Skyflow Elements + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js b/packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js similarity index 100% rename from samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js rename to packages/skyflow-js/samples/using-npm/skyflow-elements-input-formatting/src/reveal-input-formatting.js diff --git a/samples/using-npm/skyflow-elements-update-records/package.json b/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/package.json similarity index 100% rename from samples/using-npm/skyflow-elements-update-records/package.json rename to packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/package.json diff --git a/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.html b/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.html new file mode 100644 index 000000000..f749af389 --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.html @@ -0,0 +1,36 @@ + + + + + + + Skyflow Elements + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + diff --git a/samples/using-npm/skyflow-elements-update-records/src/index.js b/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.js similarity index 90% rename from samples/using-npm/skyflow-elements-update-records/src/index.js rename to packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.js index 7c4affa07..ef84f0201 100644 --- a/samples/using-npm/skyflow-elements-update-records/src/index.js +++ b/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.js @@ -68,7 +68,7 @@ try { ...collectStylesOptions, placeholder: 'card number', label: 'Card Number', - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update type: Skyflow.ElementType.CARD_NUMBER, }); @@ -79,7 +79,7 @@ try { label: 'Cvv', placeholder: 'cvv', type: Skyflow.ElementType.CVV, - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update }); const expiryDateElement = collectContainer.create({ @@ -89,7 +89,7 @@ try { label: 'Expiry Date', placeholder: 'MM/YYYY', type: Skyflow.ElementType.EXPIRATION_DATE, - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update }); const cardHolderNameElement = collectContainer.create({ @@ -116,7 +116,7 @@ try { { table: 'table1', fields: { - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update gender: 'MALE', }, }, diff --git a/samples/using-typescript/skyflow-elements-update/.gitignore b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/.gitignore similarity index 100% rename from samples/using-typescript/skyflow-elements-update/.gitignore rename to packages/skyflow-js/samples/using-npm/skyflow-elements-update/.gitignore diff --git a/samples/using-npm/skyflow-elements-update/package.json b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/package.json similarity index 75% rename from samples/using-npm/skyflow-elements-update/package.json rename to packages/skyflow-js/samples/using-npm/skyflow-elements-update/package.json index 0a55566e4..fe22b2e48 100644 --- a/samples/using-npm/skyflow-elements-update/package.json +++ b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "start": "parcel src/index.html --open", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -11,5 +12,8 @@ "license": "ISC", "dependencies": { "skyflow-js": "^1.34.0" + }, + "devDependencies": { + "parcel": "^2.0.1" } } diff --git a/packages/skyflow-js/samples/using-npm/skyflow-elements-update/src/index.html b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/src/index.html new file mode 100644 index 000000000..b7f057ead --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/src/index.html @@ -0,0 +1,52 @@ + + + + + + + Skyflow Elements Update + + + + +
    +

    Collect Elements

    +
    +
    +
    +
    +
    + + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + + +
    +
    + + + diff --git a/samples/using-npm/skyflow-elements-update/src/index.js b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/src/index.js similarity index 100% rename from samples/using-npm/skyflow-elements-update/src/index.js rename to packages/skyflow-js/samples/using-npm/skyflow-elements-update/src/index.js diff --git a/samples/using-npm/skyflow-elements/package.json b/packages/skyflow-js/samples/using-npm/skyflow-elements/package.json similarity index 100% rename from samples/using-npm/skyflow-elements/package.json rename to packages/skyflow-js/samples/using-npm/skyflow-elements/package.json diff --git a/packages/skyflow-js/samples/using-npm/skyflow-elements/src/index.html b/packages/skyflow-js/samples/using-npm/skyflow-elements/src/index.html new file mode 100644 index 000000000..513acbe00 --- /dev/null +++ b/packages/skyflow-js/samples/using-npm/skyflow-elements/src/index.html @@ -0,0 +1,51 @@ + + + + + + + Skyflow Elements + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + diff --git a/samples/using-npm/skyflow-elements/src/index.js b/packages/skyflow-js/samples/using-npm/skyflow-elements/src/index.js similarity index 100% rename from samples/using-npm/skyflow-elements/src/index.js rename to packages/skyflow-js/samples/using-npm/skyflow-elements/src/index.js diff --git a/samples/using-script-tag/3ds-helper-functions.html b/packages/skyflow-js/samples/using-script-tag/3ds-helper-functions.html similarity index 100% rename from samples/using-script-tag/3ds-helper-functions.html rename to packages/skyflow-js/samples/using-script-tag/3ds-helper-functions.html diff --git a/samples/using-script-tag/bearer-token-with-context.html b/packages/skyflow-js/samples/using-script-tag/bearer-token-with-context.html similarity index 100% rename from samples/using-script-tag/bearer-token-with-context.html rename to packages/skyflow-js/samples/using-script-tag/bearer-token-with-context.html diff --git a/samples/using-script-tag/card-brand-choice.html b/packages/skyflow-js/samples/using-script-tag/card-brand-choice.html similarity index 100% rename from samples/using-script-tag/card-brand-choice.html rename to packages/skyflow-js/samples/using-script-tag/card-brand-choice.html diff --git a/samples/using-script-tag/collect-element-listeners.html b/packages/skyflow-js/samples/using-script-tag/collect-element-listeners.html similarity index 100% rename from samples/using-script-tag/collect-element-listeners.html rename to packages/skyflow-js/samples/using-script-tag/collect-element-listeners.html diff --git a/samples/using-script-tag/collect-elements-input-formatting.html b/packages/skyflow-js/samples/using-script-tag/collect-elements-input-formatting.html similarity index 100% rename from samples/using-script-tag/collect-elements-input-formatting.html rename to packages/skyflow-js/samples/using-script-tag/collect-elements-input-formatting.html diff --git a/samples/using-script-tag/collect-elements.html b/packages/skyflow-js/samples/using-script-tag/collect-elements.html similarity index 100% rename from samples/using-script-tag/collect-elements.html rename to packages/skyflow-js/samples/using-script-tag/collect-elements.html diff --git a/samples/using-script-tag/composable-elements-update.html b/packages/skyflow-js/samples/using-script-tag/composable-elements-update.html similarity index 100% rename from samples/using-script-tag/composable-elements-update.html rename to packages/skyflow-js/samples/using-script-tag/composable-elements-update.html diff --git a/samples/using-script-tag/composable-elements.html b/packages/skyflow-js/samples/using-script-tag/composable-elements.html similarity index 100% rename from samples/using-script-tag/composable-elements.html rename to packages/skyflow-js/samples/using-script-tag/composable-elements.html diff --git a/samples/using-script-tag/composable-file-upload.html b/packages/skyflow-js/samples/using-script-tag/composable-file-upload.html similarity index 98% rename from samples/using-script-tag/composable-file-upload.html rename to packages/skyflow-js/samples/using-script-tag/composable-file-upload.html index 4f852ef45..b49e50aea 100644 --- a/samples/using-script-tag/composable-file-upload.html +++ b/packages/skyflow-js/samples/using-script-tag/composable-file-upload.html @@ -154,7 +154,7 @@

    Collect Composable Elements

    column: 'file', ...collectStylesOptions, type: Skyflow.ElementType.FILE_INPUT, - skyflowID: '', + skyflowID: '', // Replace with the Skyflow ID of the record to attach the file to }, options ); diff --git a/samples/using-script-tag/composable-multi-file-upload.html b/packages/skyflow-js/samples/using-script-tag/composable-multi-file-upload.html similarity index 100% rename from samples/using-script-tag/composable-multi-file-upload.html rename to packages/skyflow-js/samples/using-script-tag/composable-multi-file-upload.html diff --git a/samples/using-script-tag/composable-reveal.html b/packages/skyflow-js/samples/using-script-tag/composable-reveal.html similarity index 100% rename from samples/using-script-tag/composable-reveal.html rename to packages/skyflow-js/samples/using-script-tag/composable-reveal.html diff --git a/samples/using-script-tag/custom-network-message.html b/packages/skyflow-js/samples/using-script-tag/custom-network-message.html similarity index 100% rename from samples/using-script-tag/custom-network-message.html rename to packages/skyflow-js/samples/using-script-tag/custom-network-message.html diff --git a/samples/using-script-tag/custom-validations.html b/packages/skyflow-js/samples/using-script-tag/custom-validations.html similarity index 100% rename from samples/using-script-tag/custom-validations.html rename to packages/skyflow-js/samples/using-script-tag/custom-validations.html diff --git a/samples/using-script-tag/delete-pure-js.html b/packages/skyflow-js/samples/using-script-tag/delete-pure-js.html similarity index 100% rename from samples/using-script-tag/delete-pure-js.html rename to packages/skyflow-js/samples/using-script-tag/delete-pure-js.html diff --git a/samples/using-script-tag/file-render.html b/packages/skyflow-js/samples/using-script-tag/file-render.html similarity index 100% rename from samples/using-script-tag/file-render.html rename to packages/skyflow-js/samples/using-script-tag/file-render.html diff --git a/samples/using-script-tag/get-pure-js.html b/packages/skyflow-js/samples/using-script-tag/get-pure-js.html similarity index 100% rename from samples/using-script-tag/get-pure-js.html rename to packages/skyflow-js/samples/using-script-tag/get-pure-js.html diff --git a/samples/using-script-tag/masking.html b/packages/skyflow-js/samples/using-script-tag/masking.html similarity index 100% rename from samples/using-script-tag/masking.html rename to packages/skyflow-js/samples/using-script-tag/masking.html diff --git a/samples/using-script-tag/pure-js.html b/packages/skyflow-js/samples/using-script-tag/pure-js.html similarity index 100% rename from samples/using-script-tag/pure-js.html rename to packages/skyflow-js/samples/using-script-tag/pure-js.html diff --git a/samples/using-script-tag/pure-update.html b/packages/skyflow-js/samples/using-script-tag/pure-update.html similarity index 100% rename from samples/using-script-tag/pure-update.html rename to packages/skyflow-js/samples/using-script-tag/pure-update.html diff --git a/samples/using-script-tag/reveal-elements-input-formatting.html b/packages/skyflow-js/samples/using-script-tag/reveal-elements-input-formatting.html similarity index 100% rename from samples/using-script-tag/reveal-elements-input-formatting.html rename to packages/skyflow-js/samples/using-script-tag/reveal-elements-input-formatting.html diff --git a/samples/using-script-tag/skyflow-elements-update-records.html b/packages/skyflow-js/samples/using-script-tag/skyflow-elements-update-records.html similarity index 93% rename from samples/using-script-tag/skyflow-elements-update-records.html rename to packages/skyflow-js/samples/using-script-tag/skyflow-elements-update-records.html index 0dfa8a04e..9e6933b3f 100644 --- a/samples/using-script-tag/skyflow-elements-update-records.html +++ b/packages/skyflow-js/samples/using-script-tag/skyflow-elements-update-records.html @@ -103,7 +103,7 @@

    Collect Elements

    ...collectStylesOptions, placeholder: "card number", label: "Card Number", - skyflowID: "", + skyflowID: "", // Replace with a valid Skyflow ID of the record to update type: Skyflow.ElementType.CARD_NUMBER, }); @@ -114,7 +114,7 @@

    Collect Elements

    label: "Cvv", placeholder: "cvv", type: Skyflow.ElementType.CVV, - skyflowID: "", + skyflowID: "", // Replace with a valid Skyflow ID of the record to update }); const expiryDateElement = collectContainer.create({ @@ -124,7 +124,7 @@

    Collect Elements

    label: "Expiry Date", placeholder: "MM/YYYY", type: Skyflow.ElementType.EXPIRATION_DATE, - skyflowID: "", + skyflowID: "", // Replace with a valid Skyflow ID of the record to update }); const cardHolderNameElement = collectContainer.create({ @@ -151,7 +151,7 @@

    Collect Elements

    { table: "table1", fields: { - skyflowID: "", + skyflowID: "", // Replace with a valid Skyflow ID of the record to update gender: "MALE", }, }, diff --git a/samples/using-script-tag/skyflow-elements-update.html b/packages/skyflow-js/samples/using-script-tag/skyflow-elements-update.html similarity index 100% rename from samples/using-script-tag/skyflow-elements-update.html rename to packages/skyflow-js/samples/using-script-tag/skyflow-elements-update.html diff --git a/samples/using-script-tag/skyflow-elements.html b/packages/skyflow-js/samples/using-script-tag/skyflow-elements.html similarity index 100% rename from samples/using-script-tag/skyflow-elements.html rename to packages/skyflow-js/samples/using-script-tag/skyflow-elements.html diff --git a/samples/using-script-tag/skyflow-file-upload.html b/packages/skyflow-js/samples/using-script-tag/skyflow-file-upload.html similarity index 97% rename from samples/using-script-tag/skyflow-file-upload.html rename to packages/skyflow-js/samples/using-script-tag/skyflow-file-upload.html index 849303f88..98c919041 100644 --- a/samples/using-script-tag/skyflow-file-upload.html +++ b/packages/skyflow-js/samples/using-script-tag/skyflow-file-upload.html @@ -134,7 +134,7 @@

    Collect Elements

    column: 'file', ...collectStylesOptions, type: Skyflow.ElementType.FILE_INPUT, - skyflowID: '', + skyflowID: '', // Replace with the Skyflow ID of the record to attach the file to }, options ); diff --git a/samples/using-script-tag/upsert-support.html b/packages/skyflow-js/samples/using-script-tag/upsert-support.html similarity index 100% rename from samples/using-script-tag/upsert-support.html rename to packages/skyflow-js/samples/using-script-tag/upsert-support.html diff --git a/samples/using-typescript/3ds-helper-functions/package.json b/packages/skyflow-js/samples/using-typescript/3ds-helper-functions/package.json similarity index 100% rename from samples/using-typescript/3ds-helper-functions/package.json rename to packages/skyflow-js/samples/using-typescript/3ds-helper-functions/package.json diff --git a/samples/using-typescript/3ds-helper-functions/src/index.html b/packages/skyflow-js/samples/using-typescript/3ds-helper-functions/src/index.html similarity index 100% rename from samples/using-typescript/3ds-helper-functions/src/index.html rename to packages/skyflow-js/samples/using-typescript/3ds-helper-functions/src/index.html diff --git a/samples/using-typescript/3ds-helper-functions/src/index.ts b/packages/skyflow-js/samples/using-typescript/3ds-helper-functions/src/index.ts similarity index 100% rename from samples/using-typescript/3ds-helper-functions/src/index.ts rename to packages/skyflow-js/samples/using-typescript/3ds-helper-functions/src/index.ts diff --git a/packages/skyflow-js/samples/using-typescript/README.md b/packages/skyflow-js/samples/using-typescript/README.md new file mode 100644 index 000000000..cbb8330aa --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/README.md @@ -0,0 +1,10 @@ +### Running the Samples + +install the dependencies +``` +$ npm install +``` +run the sample +``` +$ npm start +``` \ No newline at end of file diff --git a/samples/using-typescript/Reveal-composable/package.json b/packages/skyflow-js/samples/using-typescript/Reveal-composable/package.json similarity index 100% rename from samples/using-typescript/Reveal-composable/package.json rename to packages/skyflow-js/samples/using-typescript/Reveal-composable/package.json diff --git a/packages/skyflow-js/samples/using-typescript/Reveal-composable/src/index.html b/packages/skyflow-js/samples/using-typescript/Reveal-composable/src/index.html new file mode 100644 index 000000000..9dae5bf55 --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/Reveal-composable/src/index.html @@ -0,0 +1,31 @@ + + + + + + + Skyflow Elements + + + + +
    +

    Reveal Elements

    +
    + +
    +
    + + + + diff --git a/samples/using-typescript/Reveal-composable/src/index.ts b/packages/skyflow-js/samples/using-typescript/Reveal-composable/src/index.ts similarity index 100% rename from samples/using-typescript/Reveal-composable/src/index.ts rename to packages/skyflow-js/samples/using-typescript/Reveal-composable/src/index.ts diff --git a/samples/using-typescript/collect-element-listeners/package.json b/packages/skyflow-js/samples/using-typescript/collect-element-listeners/package.json similarity index 100% rename from samples/using-typescript/collect-element-listeners/package.json rename to packages/skyflow-js/samples/using-typescript/collect-element-listeners/package.json diff --git a/packages/skyflow-js/samples/using-typescript/collect-element-listeners/src/index.html b/packages/skyflow-js/samples/using-typescript/collect-element-listeners/src/index.html new file mode 100644 index 000000000..ec64d0c6b --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/collect-element-listeners/src/index.html @@ -0,0 +1,41 @@ + + + + + + + Collect Element Listeners + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + + diff --git a/samples/using-typescript/collect-element-listeners/src/index.ts b/packages/skyflow-js/samples/using-typescript/collect-element-listeners/src/index.ts similarity index 100% rename from samples/using-typescript/collect-element-listeners/src/index.ts rename to packages/skyflow-js/samples/using-typescript/collect-element-listeners/src/index.ts diff --git a/samples/using-typescript/composable-elements-update/package.json b/packages/skyflow-js/samples/using-typescript/composable-elements-update/package.json similarity index 100% rename from samples/using-typescript/composable-elements-update/package.json rename to packages/skyflow-js/samples/using-typescript/composable-elements-update/package.json diff --git a/packages/skyflow-js/samples/using-typescript/composable-elements-update/src/index.html b/packages/skyflow-js/samples/using-typescript/composable-elements-update/src/index.html new file mode 100644 index 000000000..10d2117b1 --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/composable-elements-update/src/index.html @@ -0,0 +1,54 @@ + + + + + + + + Skyflow Elements + + + + +

    Composable Elements

    +
    +
    +
    + + +
    + +
    +
    
    +        
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/samples/using-typescript/composable-elements-update/src/index.ts b/packages/skyflow-js/samples/using-typescript/composable-elements-update/src/index.ts similarity index 100% rename from samples/using-typescript/composable-elements-update/src/index.ts rename to packages/skyflow-js/samples/using-typescript/composable-elements-update/src/index.ts diff --git a/samples/using-typescript/composable-elements/package.json b/packages/skyflow-js/samples/using-typescript/composable-elements/package.json similarity index 100% rename from samples/using-typescript/composable-elements/package.json rename to packages/skyflow-js/samples/using-typescript/composable-elements/package.json diff --git a/packages/skyflow-js/samples/using-typescript/composable-elements/src/index.html b/packages/skyflow-js/samples/using-typescript/composable-elements/src/index.html new file mode 100644 index 000000000..0f7c8cffc --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/composable-elements/src/index.html @@ -0,0 +1,52 @@ + + + + + + + + Skyflow Elements + + + + +

    Composable Elements

    +
    +
    +
    + +
    + +
    +
    
    +		
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/samples/using-typescript/composable-elements/src/index.ts b/packages/skyflow-js/samples/using-typescript/composable-elements/src/index.ts similarity index 100% rename from samples/using-typescript/composable-elements/src/index.ts rename to packages/skyflow-js/samples/using-typescript/composable-elements/src/index.ts diff --git a/samples/using-typescript/custom-validations/package.json b/packages/skyflow-js/samples/using-typescript/custom-validations/package.json similarity index 100% rename from samples/using-typescript/custom-validations/package.json rename to packages/skyflow-js/samples/using-typescript/custom-validations/package.json diff --git a/packages/skyflow-js/samples/using-typescript/custom-validations/src/index.html b/packages/skyflow-js/samples/using-typescript/custom-validations/src/index.html new file mode 100644 index 000000000..cdbb516ed --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/custom-validations/src/index.html @@ -0,0 +1,34 @@ + + + + + + + Custom Validations + + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    + + + + diff --git a/samples/using-typescript/custom-validations/src/index.ts b/packages/skyflow-js/samples/using-typescript/custom-validations/src/index.ts similarity index 100% rename from samples/using-typescript/custom-validations/src/index.ts rename to packages/skyflow-js/samples/using-typescript/custom-validations/src/index.ts diff --git a/samples/using-typescript/file-render/package.json b/packages/skyflow-js/samples/using-typescript/file-render/package.json similarity index 100% rename from samples/using-typescript/file-render/package.json rename to packages/skyflow-js/samples/using-typescript/file-render/package.json diff --git a/samples/using-typescript/file-render/src/index.html b/packages/skyflow-js/samples/using-typescript/file-render/src/index.html similarity index 100% rename from samples/using-typescript/file-render/src/index.html rename to packages/skyflow-js/samples/using-typescript/file-render/src/index.html diff --git a/samples/using-typescript/file-render/src/index.ts b/packages/skyflow-js/samples/using-typescript/file-render/src/index.ts similarity index 100% rename from samples/using-typescript/file-render/src/index.ts rename to packages/skyflow-js/samples/using-typescript/file-render/src/index.ts diff --git a/packages/skyflow-js/samples/using-typescript/pure-js-delete/.gitignore b/packages/skyflow-js/samples/using-typescript/pure-js-delete/.gitignore new file mode 100644 index 000000000..c34cf4310 --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/pure-js-delete/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.parcel-cache/ +dist/ +package-lock.json \ No newline at end of file diff --git a/samples/using-typescript/pure-js-delete/package.json b/packages/skyflow-js/samples/using-typescript/pure-js-delete/package.json similarity index 100% rename from samples/using-typescript/pure-js-delete/package.json rename to packages/skyflow-js/samples/using-typescript/pure-js-delete/package.json diff --git a/samples/using-typescript/pure-js-delete/src/index.html b/packages/skyflow-js/samples/using-typescript/pure-js-delete/src/index.html similarity index 100% rename from samples/using-typescript/pure-js-delete/src/index.html rename to packages/skyflow-js/samples/using-typescript/pure-js-delete/src/index.html diff --git a/samples/using-typescript/pure-js-delete/src/index.ts b/packages/skyflow-js/samples/using-typescript/pure-js-delete/src/index.ts similarity index 100% rename from samples/using-typescript/pure-js-delete/src/index.ts rename to packages/skyflow-js/samples/using-typescript/pure-js-delete/src/index.ts diff --git a/samples/using-typescript/pure-js-get/package.json b/packages/skyflow-js/samples/using-typescript/pure-js-get/package.json similarity index 100% rename from samples/using-typescript/pure-js-get/package.json rename to packages/skyflow-js/samples/using-typescript/pure-js-get/package.json diff --git a/samples/using-typescript/pure-js-get/src/index.html b/packages/skyflow-js/samples/using-typescript/pure-js-get/src/index.html similarity index 100% rename from samples/using-typescript/pure-js-get/src/index.html rename to packages/skyflow-js/samples/using-typescript/pure-js-get/src/index.html diff --git a/samples/using-typescript/pure-js-get/src/index.ts b/packages/skyflow-js/samples/using-typescript/pure-js-get/src/index.ts similarity index 100% rename from samples/using-typescript/pure-js-get/src/index.ts rename to packages/skyflow-js/samples/using-typescript/pure-js-get/src/index.ts diff --git a/samples/using-typescript/pure-js-update/package.json b/packages/skyflow-js/samples/using-typescript/pure-js-update/package.json similarity index 100% rename from samples/using-typescript/pure-js-update/package.json rename to packages/skyflow-js/samples/using-typescript/pure-js-update/package.json diff --git a/samples/using-typescript/pure-js-update/src/index.html b/packages/skyflow-js/samples/using-typescript/pure-js-update/src/index.html similarity index 100% rename from samples/using-typescript/pure-js-update/src/index.html rename to packages/skyflow-js/samples/using-typescript/pure-js-update/src/index.html diff --git a/samples/using-typescript/pure-js-update/src/index.ts b/packages/skyflow-js/samples/using-typescript/pure-js-update/src/index.ts similarity index 96% rename from samples/using-typescript/pure-js-update/src/index.ts rename to packages/skyflow-js/samples/using-typescript/pure-js-update/src/index.ts index c4ce7d5c8..69aa7b4ca 100644 --- a/samples/using-typescript/pure-js-update/src/index.ts +++ b/packages/skyflow-js/samples/using-typescript/pure-js-update/src/index.ts @@ -2,7 +2,6 @@ Copyright (c) 2025 Skyflow, Inc. */ import Skyflow, { - updateResponse, SkyflowConfig, UpdateRequest, UpdateResponse, @@ -72,14 +71,14 @@ try { element.innerHTML = JSON.stringify(res, null, 2); } }, - (err: updateResponse) => { + (err: UpdateResponse) => { const element = document.getElementById('updateResponse') as HTMLElement; if (element) { element.innerHTML = JSON.stringify(err, null, 2); } } ) - .catch((err: updateResponse) => { + .catch((err: UpdateResponse) => { const element = document.getElementById('updateResponse') as HTMLElement; if (element) { element.innerHTML = JSON.stringify(err, null, 2); diff --git a/samples/using-typescript/pure-js/package.json b/packages/skyflow-js/samples/using-typescript/pure-js/package.json similarity index 100% rename from samples/using-typescript/pure-js/package.json rename to packages/skyflow-js/samples/using-typescript/pure-js/package.json diff --git a/samples/using-typescript/pure-js/src/index.html b/packages/skyflow-js/samples/using-typescript/pure-js/src/index.html similarity index 100% rename from samples/using-typescript/pure-js/src/index.html rename to packages/skyflow-js/samples/using-typescript/pure-js/src/index.html diff --git a/samples/using-typescript/pure-js/src/index.ts b/packages/skyflow-js/samples/using-typescript/pure-js/src/index.ts similarity index 100% rename from samples/using-typescript/pure-js/src/index.ts rename to packages/skyflow-js/samples/using-typescript/pure-js/src/index.ts diff --git a/samples/using-typescript/skyflow-elements-input-formatting/package.json b/packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/package.json similarity index 100% rename from samples/using-typescript/skyflow-elements-input-formatting/package.json rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/package.json diff --git a/samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts b/packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts similarity index 100% rename from samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/collect-input-formatting.ts diff --git a/packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/index.html b/packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/index.html new file mode 100644 index 000000000..9c287b53f --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/index.html @@ -0,0 +1,52 @@ + + + + + + + Skyflow Elements + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + + diff --git a/samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts b/packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts similarity index 100% rename from samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-input-formatting/src/reveal-input-formatting.ts diff --git a/samples/using-typescript/skyflow-elements-update-records/package.json b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/package.json similarity index 100% rename from samples/using-typescript/skyflow-elements-update-records/package.json rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/package.json diff --git a/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.html b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.html new file mode 100644 index 000000000..78be05084 --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.html @@ -0,0 +1,36 @@ + + + + + + + Skyflow Elements + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + + + diff --git a/samples/using-typescript/skyflow-elements-update-records/src/index.ts b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts similarity index 92% rename from samples/using-typescript/skyflow-elements-update-records/src/index.ts rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts index 52af7456b..e81e189d0 100644 --- a/samples/using-typescript/skyflow-elements-update-records/src/index.ts +++ b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts @@ -82,7 +82,7 @@ try { ...collectStylesOptions, placeholder: 'card number', label: 'Card Number', - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update type: Skyflow.ElementType.CARD_NUMBER, }; const cardNumberElement: CollectElement = collectContainer.create(cardNumberInput); @@ -94,7 +94,7 @@ try { label: 'Cvv', placeholder: 'cvv', type: Skyflow.ElementType.CVV, - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update }; const cvvElement: CollectElement = collectContainer.create(cvvInput); @@ -105,7 +105,7 @@ try { label: 'Expiry Date', placeholder: 'MM/YYYY', type: Skyflow.ElementType.EXPIRATION_DATE, - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update }; const expiryDateElement: CollectElement = collectContainer.create(expiryDateInput); @@ -131,7 +131,7 @@ try { { table: 'table1', fields: { - skyflowID: '', + skyflowID: '', // Replace with a valid Skyflow ID of the record to update gender: 'MALE', }, }, diff --git a/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/.gitignore b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/.gitignore new file mode 100644 index 000000000..c34cf4310 --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.parcel-cache/ +dist/ +package-lock.json \ No newline at end of file diff --git a/samples/using-typescript/skyflow-elements-update/package.json b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/package.json similarity index 100% rename from samples/using-typescript/skyflow-elements-update/package.json rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-update/package.json diff --git a/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/src/index.html b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/src/index.html new file mode 100644 index 000000000..9bd5c28aa --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/src/index.html @@ -0,0 +1,52 @@ + + + + + + + Skyflow Elements Update + + + + +
    +

    Collect Elements

    +
    +
    +
    +
    +
    + + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + + +
    +
    + + + diff --git a/samples/using-typescript/skyflow-elements-update/src/index.ts b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update/src/index.ts similarity index 100% rename from samples/using-typescript/skyflow-elements-update/src/index.ts rename to packages/skyflow-js/samples/using-typescript/skyflow-elements-update/src/index.ts diff --git a/samples/using-typescript/skyflow-elements/package.json b/packages/skyflow-js/samples/using-typescript/skyflow-elements/package.json similarity index 100% rename from samples/using-typescript/skyflow-elements/package.json rename to packages/skyflow-js/samples/using-typescript/skyflow-elements/package.json diff --git a/packages/skyflow-js/samples/using-typescript/skyflow-elements/src/index.html b/packages/skyflow-js/samples/using-typescript/skyflow-elements/src/index.html new file mode 100644 index 000000000..1bc946bf5 --- /dev/null +++ b/packages/skyflow-js/samples/using-typescript/skyflow-elements/src/index.html @@ -0,0 +1,51 @@ + + + + + + + Skyflow Elements + + + +

    Collect Elements

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    
    +      
    +
    + +
    +

    Reveal Elements

    +
    +
    +
    +
    +
    + +
    +
    + + + + diff --git a/samples/using-typescript/skyflow-elements/src/index.ts b/packages/skyflow-js/samples/using-typescript/skyflow-elements/src/index.ts similarity index 100% rename from samples/using-typescript/skyflow-elements/src/index.ts rename to packages/skyflow-js/samples/using-typescript/skyflow-elements/src/index.ts diff --git a/src/core-utils/collect.ts b/packages/skyflow-js/src/api-utils/collect.ts similarity index 76% rename from src/core-utils/collect.ts rename to packages/skyflow-js/src/api-utils/collect.ts index ef8a9a468..aaefe761b 100644 --- a/src/core-utils/collect.ts +++ b/packages/skyflow-js/src/api-utils/collect.ts @@ -1,14 +1,11 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import merge from 'lodash/merge'; import omit from 'lodash/omit'; -import get from 'lodash/get'; -import Client from '../client'; -import SkyflowError from '../libs/skyflow-error'; -import { getAccessToken } from '../utils/bus-events'; +import { getAccessToken } from '@core/utils/bus-events'; +import Client from '@core/client'; import { - IInsertRecordInput, IInsertRecord, IValidationRule, ValidationRuleType, + IInsertRecordInput, IInsertRecord, MessageType, LogLevel, InsertResponse, IUpdateRequest, @@ -16,10 +13,8 @@ import { UpdateResponse, UpdateResponseType, } from '../utils/common'; -import SKYFLOW_ERROR_CODE from '../utils/constants'; import { printLog } from '../utils/logs-helper'; -import IFrameFormElement from '../core/internal/iframe-form'; -import { BatchInsertRequestBody } from '../core/internal/internal-types'; +import { BatchInsertRequestBody } from '../internal/internal-types'; export interface IUpsertOptions{ table: string, @@ -166,78 +161,9 @@ export const constructUploadResponse = (response) => { return JSON.stringify({ skyflow_id: data.skyflowID }) as any; }; -const keyify = (obj, prefix = '') => Object.keys(obj).reduce((res: any, el) => { - if (Array.isArray(obj[el])) { - return [...res, prefix + el]; - } if (typeof obj[el] === 'object' && obj[el] !== null) { - return [...res, ...keyify(obj[el], `${prefix + el}.`)]; - } - return [...res, prefix + el]; -}, []); - -const checkDuplicateColumns = (additionalColumns, columns, table) => { - const keys = keyify(additionalColumns); - keys.forEach((key) => { - const value = get(columns, key); - if (value) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, [`${key}`, `${table}`], true); - } - }); -}; - -export const constructElementsInsertReq = (req, update, options) => { - let tables = Object.keys(req); - let ids = Object.keys(update); - const additionalFields = options?.additionalFields; - if (additionalFields) { - // merge additionalFields in req - additionalFields.records.forEach((record) => { - if (record.fields.skyflowID) { - if (ids.includes(record.fields.skyflowID)) { - checkDuplicateColumns( - record.fields, update[record.fields.skyflowID], record.table, - ); - const temp = record.fields; - merge(temp, update[record.fields.skyflowID]); - update[record.fields.skyflowID] = temp; - } else { - update[record.fields.skyflowID] = { - ...record.fields, - table: record.table, - }; - } - } else if (!record.fields.skyflowID) { - if (tables.includes(record.table)) { - checkDuplicateColumns(record.fields, req[record.table], record.table); - const temp = record.fields; - merge(temp, req[record.table]); - req[record.table] = temp; - } else { - req[record.table] = record.fields; - } - } - }); - } - const records: IInsertRecord[] = []; - const updateRecords: IInsertRecord[] = []; +// keyify / checkDuplicateColumns / constructElementsInsertReq moved to +// @core/api-utils/collect (variant-neutral request assembly). - tables = Object.keys(req); - tables.forEach((table) => { - records.push({ - table, - fields: req[table], - }); - }); - ids = Object.keys(update); - ids.forEach((id) => { - updateRecords.push({ - table: update[id].table, - fields: update[id], - skyflowID: id, - }); - }); - return [{ records }, { updateRecords }]; -}; const updateRecordsInVault = ( skyflowIdRecord: IInsertRecord, client: Client, @@ -468,24 +394,5 @@ export const insertDataInMultipleFiles = async ( }); }); -export const checkForElementMatchRule = (validations: IValidationRule[]) => { - if (!validations) return false; - for (let i = 0; i < validations.length; i += 1) { - if (validations[i].type === ValidationRuleType.ELEMENT_VALUE_MATCH_RULE) { - return true; - } - } - return false; -}; - -export const checkForValueMatch = (validations: IValidationRule[], element: IFrameFormElement) => { - if (!validations) return false; - for (let i = 0; i < validations.length; i += 1) { - if (validations[i].type === ValidationRuleType.ELEMENT_VALUE_MATCH_RULE) { - if (element && !element.isMatchEqual(i, element.state.value, validations[i])) { - return true; - } - } - } - return false; -}; +// checkForElementMatchRule / checkForValueMatch moved to @core/helpers +// (variant-neutral frame leaf-helpers). Frame code imports them from @core. diff --git a/src/core-utils/delete.ts b/packages/skyflow-js/src/api-utils/delete.ts similarity index 95% rename from src/core-utils/delete.ts rename to packages/skyflow-js/src/api-utils/delete.ts index 0b6a6a0f6..0aec52553 100644 --- a/src/core-utils/delete.ts +++ b/packages/skyflow-js/src/api-utils/delete.ts @@ -1,6 +1,6 @@ -import Client from '../client'; -import SkyflowError from '../libs/skyflow-error'; -import { getAccessToken } from '../utils/bus-events'; +import { getAccessToken } from '@core/utils/bus-events'; +import SkyflowError from '@core/errors'; +import Client from '@core/client'; import { IDeleteOptions, IDeleteRecord, diff --git a/src/core-utils/reveal.ts b/packages/skyflow-js/src/api-utils/reveal.ts similarity index 93% rename from src/core-utils/reveal.ts rename to packages/skyflow-js/src/api-utils/reveal.ts index c37a94cf4..01e01700a 100644 --- a/src/core-utils/reveal.ts +++ b/packages/skyflow-js/src/api-utils/reveal.ts @@ -2,9 +2,10 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import Client from '../client'; -import { getAccessToken } from '../utils/bus-events'; -import SkyflowError from '../libs/skyflow-error'; +import { getAccessToken } from '@core/utils/bus-events'; +import { FILE_DOWNLOAD_URL_PARAM } from '@core/constants'; +import Client from '@core/client'; +import { formatForPureJsFailure } from '@core/api-utils/reveal'; import { IRevealRecord, IRevealResponseType, MessageType, LogLevel, IGetRecord, ISkyflowIdRecord, RedactionType, @@ -19,7 +20,6 @@ import { IRevealRecordComposable, } from '../utils/common'; import { printLog } from '../utils/logs-helper'; -import { FILE_DOWNLOAD_URL_PARAM } from '../core/constants'; interface IApiSuccessResponse { records: [ @@ -37,26 +37,6 @@ const formatForPureJsSuccess = (response: IApiSuccessResponse) => { { token: record.token, value: record.value, valueType: record.valueType })); }; -const formatForPureJsFailure = (cause, tokenId:string, purejs: boolean) => { - if (purejs) { - return { - token: tokenId, - error: { - code: cause?.error?.code, - description: cause?.error?.description, - }, - }; - } - return ({ - token: tokenId, - ...new SkyflowError({ - code: cause?.error?.code, - description: cause?.error?.description, - type: cause?.error?.type, - }, [], true), - }); -}; - const formatForRenderFileFailure = (cause, skyflowID:string, column: string) => ({ skyflowId: skyflowID, column, @@ -337,29 +317,7 @@ export const fetchRecordsByTokenIdComposable = ( }); }); -export const formatRecordsForIframe = (response: IRevealResponseType) => { - const result: Record = {}; - if (response.records) { - response.records.forEach((record) => { - const key = record.token; - const recordData = { - value: record.value, - redaction: record.redaction, - }; - - if (result[key]) { - if (Array.isArray(result[key])) { - result[key].push(recordData); - } else { - result[key] = [result[key], recordData]; - } - } else { - result[key] = recordData; - } - }); - } - return result; -}; +// formatRecordsForIframe moved to @core/api-utils/reveal (variant-neutral). export const formatRecordsForRender = (response : IRenderResponseType, column, skyflowID) => { let url = ''; if (response.fields) { diff --git a/packages/skyflow-js/src/external/collect/collect-container.ts b/packages/skyflow-js/src/external/collect/collect-container.ts new file mode 100644 index 000000000..08eb99d54 --- /dev/null +++ b/packages/skyflow-js/src/external/collect/collect-container.ts @@ -0,0 +1,170 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB collect container: the shared @core CollectContainer base bound to +// privacyDB's collect option/response types, plus the injected divergence — +// create() (uses privacyDB's collect-input validator, no tableName→table remap) +// and the A-only uploadFiles (file upload is privacyDB-only). Token handling and +// error mapping use the base defaults. The element interfaces are re-exported +// from @core under the same public names (imported as './collect-container'). +import bus from 'framebus'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import { + ELEMENT_EVENTS_TO_IFRAME, + COLLECT_TYPES, +} from '@core/constants'; +import properties from '@core/properties'; +import SkyflowError from '@core/errors'; +import { MessageType, VariantCollectAdapter } from '@core/types'; +import CoreCollectContainer, { + ICollectElementBase, +} from '@core/external/collect/collect-container'; +import collectVariant from './collect-variant'; +import { printLog, parameterizedString } from '../../utils/logs-helper'; +import { validateCollectElementInput, validateInitConfig } from '../../utils/validators'; +import { + CollectElementInput, + CollectElementOptions, + CollectElementUpdateOptions, + CollectResponse, + ICollectOptions, + UploadFilesResponse, +} from '../../utils/common'; + +export type { + ElementGroupItem, ElementGroup, +} from '@core/external/collect/collect-container'; + +// privacyDB collect-element descriptor: shared base + privacyDB `table` key. See 2.4. +export interface ICollectElement extends ICollectElementBase { + table?: string; +} + +const CLASS_NAME = 'CollectContainer'; +class CollectContainer extends CoreCollectContainer< +ICollectOptions, CollectResponse, CollectElementUpdateOptions, +CollectElementInput, CollectElementOptions +> { + // privacyDB collect key strategy (internal `skyflowID`/`table`); single source is + // this package's VariantAdapter, supplied here and injected into each element. + protected collectVariant: VariantCollectAdapter = collectVariant; + + protected validateCreateInput(input: CollectElementInput): void { + validateCollectElementInput(input, this.context.logLevel); + } + + // privacyDB carries the file-input `accept` list onto the element descriptor. + // eslint-disable-next-line class-methods-use-this + protected buildCreateElementFields( + _input: CollectElementInput, + options: CollectElementOptions, + ): Record { + return { accept: options.allowedFileType }; + } + + uploadFiles = (options?: ICollectOptions): Promise => { + this.isSkyflowFrameReady = this.metaData.skyflowContainer.isControllerFrameReady; + if (this.isSkyflowFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (Object.keys(this.elements).length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); + } + this.removeStaleElements(); + const fileElements = Object.values(this.elements); + const elementIds = Object.keys(this.elements); + fileElements.forEach((element) => { + if (!element.isMounted()) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + element.isValidElement(); + }); + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .emit( + ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.metaData.uuid, + { + type: COLLECT_TYPES.FILE_UPLOAD, + ...options, + elementIds, + containerId: this.containerId, + errorMessages: this.customErrorMessages, + }, + (data: any) => { + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.context.logLevel); + reject(data?.error); + } else { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + + resolve(data); + } + }, + ); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), + MessageType.LOG, this.context.logLevel); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.containerId, () => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (Object.keys(this.elements).length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); + } + this.removeStaleElements(); + const fileElements = Object.values(this.elements); + const elementIds = Object.keys(this.elements); + fileElements.forEach((element) => { + if (!element.isMounted()) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + element.isValidElement(); + }); + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .emit( + ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.metaData.uuid, + { + type: COLLECT_TYPES.FILE_UPLOAD, + ...options, + elementIds, + containerId: this.containerId, + errorMessages: this.customErrorMessages, + }, + (data: any) => { + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.context.logLevel); + reject(data?.error); + } else { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + + resolve(data); + } + }, + ); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), + MessageType.LOG, this.context.logLevel); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); + }); + }; +} +export default CollectContainer; diff --git a/packages/skyflow-js/src/external/collect/collect-variant.ts b/packages/skyflow-js/src/external/collect/collect-variant.ts new file mode 100644 index 000000000..3ada967af --- /dev/null +++ b/packages/skyflow-js/src/external/collect/collect-variant.ts @@ -0,0 +1,14 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB collect-side key normalization, bound onto the shared @core collect +// containers' `collectVariant` protected field. privacyDB consumes the internal +// `skyflowID`/`table` keys directly, so `normalizeUpdateOptions` is a no-op. +import { VariantCollectAdapter } from '@core/types'; + +const collectVariant: VariantCollectAdapter = { + normalizeUpdateOptions: () => {}, + skyflowIdKey: 'skyflowID', +}; + +export default collectVariant; diff --git a/packages/skyflow-js/src/external/collect/compose-collect-container.ts b/packages/skyflow-js/src/external/collect/compose-collect-container.ts new file mode 100644 index 000000000..a7dabcedd --- /dev/null +++ b/packages/skyflow-js/src/external/collect/compose-collect-container.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// privacyDB composable collect container: the shared @core CoreComposableCollectContainer +// (controller-frame bootstrap, mount/grid layout, createMultipleElement, collect(), +// on(), the bus COMPOSABLE_CONTAINER handshake and updateListeners) plus the +// package-only surface — create() (its typed CollectElementInput and returned +// ComposableElement) and the file-upload pieces (registerElementListeners + +// uploadFiles) that privacyDB supports and flowDB does not. +import properties from '@core/properties'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import { + ELEMENT_EVENTS_TO_IFRAME, + COLLECT_TYPES, +} from '@core/constants'; +import CoreComposableCollectContainer from '@core/external/collect/composable-collect-container'; +import SkyflowError from '@core/errors'; +import Client from '@core/client'; +import { VariantCollectAdapter } from '@core/types'; +import collectVariant from './collect-variant'; +import { + MessageType, + CollectElementInput, + CollectElementOptions, + ICollectOptions, + CollectResponse, + UploadFilesResponse, +} from '../../utils/common'; +import { printLog, parameterizedString } from '../../utils/logs-helper'; +import { validateCollectElementInput, validateInitConfig } from '../../utils/validators'; + +const CLASS_NAME = 'CollectContainer'; +class ComposableContainer extends CoreComposableCollectContainer< +ICollectOptions, CollectResponse, CollectElementInput, CollectElementOptions +> { + // privacyDB collect key strategy (`skyflowID`/`table`); injected into each element. + protected collectVariant: VariantCollectAdapter = collectVariant; + + protected validateCreateInput(input: CollectElementInput): void { + validateCollectElementInput(input, this.context.logLevel); + } + + // privacyDB composable create() adds no variant identity field (the internal + // `table`/`skyflowID` keys arrive on the input as-is). + // eslint-disable-next-line class-methods-use-this + protected buildCreateElementFields(): Record { + return {}; + } + + // Collect-only per-element file-upload wiring, run from the base mount(). + protected registerElementListeners(): void { + this.elementsList.forEach((element) => { + this.eventEmitter.on(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, (data, callback) => { + this.getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + this.emitEvent( + `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, + { + elementName: element.name, + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + containerId: this.containerId, + }, + clientConfig: { + vaultURL: this.metaData?.clientJSON?.config?.vaultURL, + vaultID: this.metaData?.clientJSON?.config?.vaultID, + authToken, + }, + options: { + ...data?.options, + }, + errorMessages: this.customErrorMessages, + }, + ); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + callback(err); + }); + }); + }); + } + + uploadFiles = (options: ICollectOptions): + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + if (!this.elementsList || this.elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const elementIds:{ frameId:string, elementId:string }[] = []; + this.elementsList.forEach((element) => { + elementIds.push({ + frameId: this.tempElements.elementName, + elementId: element.elementName ?? '', + }); + }); + const client = Client.fromJSON(this.metaData.clientJSON) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + this.emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.containerId, { + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + ...options, + // tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.containerId, + }, + clientConfig: { + vaultURL: this.metaData.clientJSON.config.vaultURL, + vaultID: this.metaData.clientJSON.config.vaultID, + authToken, + }, + errorMessages: this.customErrorMessages, + }); + window.addEventListener('message', (event) => { + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.context.logLevel); + reject(data?.error); + } else if (data?.fileUploadResponse) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.context.logLevel); + reject(data); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + }); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + } + }); +} +export default ComposableContainer; diff --git a/packages/skyflow-js/src/external/collect/compose-collect-element.ts b/packages/skyflow-js/src/external/collect/compose-collect-element.ts new file mode 100644 index 000000000..dad8a20af --- /dev/null +++ b/packages/skyflow-js/src/external/collect/compose-collect-element.ts @@ -0,0 +1,6 @@ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// The composable collect element is variant-agnostic and now lives in @core; +// re-exported here so the package path (index-node export, tests) stays stable. +export { default } from '@core/external/collect/composable-collect-element'; diff --git a/packages/skyflow-js/src/external/reveal/composable-reveal-container.ts b/packages/skyflow-js/src/external/reveal/composable-reveal-container.ts new file mode 100644 index 000000000..d7f2e7f83 --- /dev/null +++ b/packages/skyflow-js/src/external/reveal/composable-reveal-container.ts @@ -0,0 +1,46 @@ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +// privacyDB composable reveal container: the shared @core reveal base plus the +// package-only surface — create() (its typed reveal-input and the renderFile- +// capable ComposableRevealElement) and the element factory / record validator. +// Error mapping and options handling use the base (privacyDB) defaults. +import CoreComposableRevealContainer from '@core/external/reveal/composable-reveal-container'; +import ComposableRevealElement from './composable-reveal-element'; +import ComposableRevealInternalElement from './composable-reveal-internal'; +import { IRevealElementOptions } from './reveal-container'; +import { RevealElementInput, RevealResponse } from '../../index-node'; +import { validateRevealElementRecords } from '../../utils/validators'; + +class ComposableRevealContainer extends CoreComposableRevealContainer { + create = (input: RevealElementInput, options?: IRevealElementOptions) => { + const { elementName, controllerIframeName } = this.buildComposableRevealElement(input, options); + return new ComposableRevealElement(elementName, + this.eventEmitter, + controllerIframeName); + }; + + protected instantiateInternalElement( + elementId: string, + tempElements: any, + ): ComposableRevealInternalElement { + return new ComposableRevealInternalElement( + elementId, + tempElements, + this.metaData, + { + containerId: this.containerId, + isMounted: this.containerMounted, + type: this.type, + eventEmitter: this.eventEmitter, + }, + this.context, + ); + } + + // eslint-disable-next-line class-methods-use-this + protected validateRecords(records: any[]): void { + validateRevealElementRecords(records); + } +} +export default ComposableRevealContainer; diff --git a/packages/skyflow-js/src/external/reveal/composable-reveal-element.ts b/packages/skyflow-js/src/external/reveal/composable-reveal-element.ts new file mode 100644 index 000000000..fb6431702 --- /dev/null +++ b/packages/skyflow-js/src/external/reveal/composable-reveal-element.ts @@ -0,0 +1,29 @@ +import { ELEMENT_EVENTS_TO_IFRAME } from '@core/constants'; +import CoreComposableRevealElement from '@core/external/reveal/composable-reveal-element'; +import { RenderFileResponse } from '../../utils/common'; +import { IRevealElementInput } from './reveal-container'; + +// privacyDB composable reveal element: the shared @core base bound to privacyDB's +// reveal-input shape, plus the file-render request (flowDB has no renderFile). +class ComposableRevealElement extends CoreComposableRevealElement { + renderFile(): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line no-underscore-dangle + this.eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST}:${this.elementName}`, + {}, + (response) => { + if (response?.errors) { + reject(response); + } else if (response?.error) { + reject({ errors: response?.error }); + } else { + resolve(response); + } + }, + ); + }); + } +} + +export default ComposableRevealElement; diff --git a/packages/skyflow-js/src/external/reveal/composable-reveal-internal.ts b/packages/skyflow-js/src/external/reveal/composable-reveal-internal.ts new file mode 100644 index 000000000..5fb231b4b --- /dev/null +++ b/packages/skyflow-js/src/external/reveal/composable-reveal-internal.ts @@ -0,0 +1,158 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB composable reveal-internal element: the shared @core base bound to +// privacyDB's reveal-input shape, plus the file-render feature. `renderFile` +// (and the RENDER_FILE_REQUEST listener that drives it, wired through the base's +// registerRenderFileRequestListener hook) stays here because it pulls in the +// package-only render transport; flowDB is token-only and has no renderFile. +import { + ELEMENT_EVENTS_TO_IFRAME, + REVEAL_TYPES, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import properties from '@core/properties'; +import { + Context, ICoreMetadata, RevealContainerProps, +} from '@core/types'; +import CoreComposableRevealInternalElement from '@core/external/reveal/composable-reveal-internal'; +import { + MessageType, RenderFileResponse, +} from '../../utils/common'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import { parameterizedString, printLog } from '../../utils/logs-helper'; +import { validateInitConfig, validateRenderElementRecord } from '../../utils/validators'; + +const CLASS_NAME = 'RevealElementInteranalElement'; + +export interface RevealComposableGroup{ + record: IRevealElementInput + options: IRevealElementOptions +} + +class ComposableRevealInternalElement + extends CoreComposableRevealInternalElement { + #getSkyflowBearerToken: () => Promise | undefined; + + constructor(elementId: string, + recordGroup, + metaData: ICoreMetadata, + container: RevealContainerProps, + context: Context) { + super(elementId, recordGroup, metaData, container, context); + this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; + } + + protected registerRenderFileRequestListener(element: any): void { + this.eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, + (data, callback) => { + this.renderFile(element)?.then((response) => { + callback?.(response); + })?.catch((error) => { + callback?.({ error }); + }); + }, + ); + } + + // Resolves the bearer token, emits the RENDER_FILE call, and wires its response + // listener. Identical for the frame-ready and wait-for-RENDER_MOUNTED paths. + private sendRenderFileCall( + recordData: any, + altText: string, + resolve: (value: RenderFileResponse) => void, + reject: (reason?: any) => void, + ): void { + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + this.emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.metaData.clientJSON.config.vaultURL, + vaultID: this.metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window?.addEventListener('message', (event) => { + if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { + if (event?.data + && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + recordData.name) { + if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText, recordData); + } + reject(revealData?.error || revealData?.errors); + } else { + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.context.logLevel); + resolve(revealData); + } + } + } + } + }); + }).catch((err:any) => { + printLog(`${err?.message}`, MessageType.ERROR, this.context.logLevel); + reject(err); + }); + } + + renderFile(recordData: any): Promise { + let altText = ''; + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + altText = recordData.altText; + } + this.setAltText('loading...', recordData); + const loglevel = this.context.logLevel; + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + if (this.isComposableFrameReady) { + this.sendRenderFileCall(recordData, altText, resolve, reject); + } else { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + recordData?.name) { + this.markMounted(); + this.sendRenderFileCall(recordData, altText, resolve, reject); + } + }); + } + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err?.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } +} + +export default ComposableRevealInternalElement; diff --git a/packages/skyflow-js/src/external/reveal/reveal-container.ts b/packages/skyflow-js/src/external/reveal/reveal-container.ts new file mode 100644 index 000000000..c3562a20a --- /dev/null +++ b/packages/skyflow-js/src/external/reveal/reveal-container.ts @@ -0,0 +1,53 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB reveal container: the shared @core RevealContainer base bound to +// privacyDB's types, plus the injected divergence — the RevealElement factory +// and the reveal-record validator. privacyDB has no reveal options and maps +// errors through as-is, so validateOptions/wrapRevealError use the base defaults. +// The reveal input/option TYPE definitions live here (imported as +// './reveal-container' by the reveal element/composable files). +import CoreRevealContainer from '@core/external/reveal/reveal-container'; +import { + IRevealElementOptions, RedactionType, ICoreMetadata, RevealContainerProps, Context, + RevealResponse, +} from '@core/types'; +import { validateRevealElementRecords } from '../../utils/validators'; +import RevealElement from './reveal-element'; + +export interface IRevealElementInput { + token?: string; + skyflowID?: string; + table?: string; + column?: string; + redaction?: RedactionType; + inputStyles?: object; + label?: string; + labelStyles?: object; + altText?: string; + errorTextStyles?: object; +} + +// Relocated to @core/types; re-exported here under the same public name. +export type { IRevealElementOptions }; + +class RevealContainer extends + CoreRevealContainer { + // eslint-disable-next-line class-methods-use-this + protected createRevealElement( + record: IRevealElementInput, + options: IRevealElementOptions | undefined, + metaData: ICoreMetadata, + container: RevealContainerProps, + elementId: string, + context: Context, + ): RevealElement { + return new RevealElement(record, options, metaData, container, elementId, context); + } + + // eslint-disable-next-line class-methods-use-this + protected validateRecords(records: IRevealElementInput[]): void { + validateRevealElementRecords(records); + } +} +export default RevealContainer; diff --git a/packages/skyflow-js/src/external/reveal/reveal-element.ts b/packages/skyflow-js/src/external/reveal/reveal-element.ts new file mode 100644 index 000000000..ac13127c8 --- /dev/null +++ b/packages/skyflow-js/src/external/reveal/reveal-element.ts @@ -0,0 +1,114 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB reveal element: the shared @core reveal-element base bound to +// privacyDB's reveal-input shape, plus the file-render feature. `renderFile` +// stays here (not in @core) because it pulls in the package-only reveal +// transport (`formatForRenderClient`) and render validators; flowDB is +// token-only and has no renderFile. +import bus from 'framebus'; +import { + ELEMENT_EVENTS_TO_IFRAME, + REVEAL_TYPES, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import properties from '@core/properties'; +import CoreRevealElement from '@core/external/reveal/reveal-element'; +import { + MessageType, RenderFileResponse, +} from '../../utils/common'; +import { IRevealElementInput } from './reveal-container'; +import { parameterizedString, printLog } from '../../utils/logs-helper'; +import { formatForRenderClient } from '../../api-utils/reveal'; +import { validateInitConfig, validateRenderElementRecord } from '../../utils/validators'; + +const CLASS_NAME = 'RevealElement'; + +class RevealElement extends CoreRevealElement { + #isSkyflowFrameReady: boolean = false; + + // Emits the RENDER_FILE request and wires its response callback. Identical for + // the frame-ready and wait-for-ready paths, so it lives here once. + private emitRenderFileRequest( + resolve: (value: RenderFileResponse) => void, + reject: (reason?: any) => void, + altText: string, + loglevel: any, + ): void { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .emit( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.metaData.uuid, + { + type: REVEAL_TYPES.RENDER_FILE, + records: this.recordData, + containerId: this.containerId, + iframeName: this.iframe.name, + errorMessages: this.customerErrorMessages, + }, + (revealData: any) => { + if (revealData.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.context.logLevel); + if (Object.prototype.hasOwnProperty.call(this.recordData, 'altText')) { + this.setAltText(altText); + } + reject(formatForRenderClient(revealData, this.recordData.column as string)); + } else { + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, this.recordData.skyflowID), + MessageType.LOG, this.context.logLevel); + resolve(formatForRenderClient(revealData, this.recordData.column as string)); + } + }, + ); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } + + renderFile(): Promise { + this.#isSkyflowFrameReady = this.metaData.skyflowContainer.isControllerFrameReady; + let altText = ''; + if (Object.prototype.hasOwnProperty.call(this.recordData, 'altText')) { + altText = this.recordData.altText; + } + this.setAltText('loading...'); + const loglevel = this.context.logLevel; + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(this.recordData); + if (this.#isSkyflowFrameReady) { + this.emitRenderFileRequest(resolve, reject, altText, loglevel); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.metaData.uuid, + () => { + this.emitRenderFileRequest(resolve, reject, altText, loglevel); + }, + ); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } +} + +export default RevealElement; diff --git a/packages/skyflow-js/src/external/skyflow-container.ts b/packages/skyflow-js/src/external/skyflow-container.ts new file mode 100644 index 000000000..6313553b0 --- /dev/null +++ b/packages/skyflow-js/src/external/skyflow-container.ts @@ -0,0 +1,221 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import bus from 'framebus'; +import properties from '@core/properties'; +import { + ELEMENT_EVENTS_TO_IFRAME, + PUREJS_TYPES, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import CoreSkyflowContainer from '@core/external/skyflow-container'; +import { + validateInsertRecords, + validateDetokenizeInput, + validateInitConfig, + validateGetInput, + validateGetByIdInput, + validateUpsertOptions, + validateDeleteRecords, + validateUpdateRecord, +} from '../utils/validators'; +import { + printLog, + parameterizedString, +} from '../utils/logs-helper'; +import { + IDetokenizeInput, + IGetInput, + MessageType, + IGetByIdInput, + IInsertOptions, + IDeleteOptions, + IDeleteRecordInput, + IGetOptions, + InsertResponse, + GetByIdResponse, + GetResponse, + DeleteResponse, + IInsertRecordInput, + DetokenizeResponse, + IUpdateRequest, + UpdateResponse, + IUpdateOptions, +} from '../utils/common'; + +const CLASS_NAME = 'SkyflowContainer'; +// privacyDB SkyflowContainer: the shared @core controller-frame bootstrap base +// (constructor + `isControllerFrameReady`) plus the pure-JS data methods that +// flowDB does not expose. +class SkyflowContainer extends CoreSkyflowContainer { + // Fire a pure-JS request to the controller frame. When the frame is already + // ready the request emits immediately; otherwise it is deferred until the + // PUREJS_FRAME_READY handshake fires. `logError` mirrors the historical + // per-method behaviour: insert/update/delete log the error envelope before + // rejecting, the read paths (detokenize/get/getById) do not. + private emitPureJsRequest( + resolve: (value: any) => void, + reject: (reason?: any) => void, + type: string, + payload: Record, + logError: boolean = false, + ): void { + const emit = () => { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .emit( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.containerId, + { + type, + ...payload, + }, + (responseData: any) => { + if (responseData.error) { + if (logError) { + printLog(`${JSON.stringify(responseData.error)}`, + MessageType.ERROR, this.context.logLevel); + } + reject(responseData.error); + } else resolve(responseData); + }, + ); + }; + if (this.isControllerFrameReady) { + emit(); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.containerId, emit); + } + // Emitted synchronously in both branches, preserving the original log timing. + printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, type), + MessageType.LOG, this.context.logLevel); + } + + detokenize(detokenizeInput: IDetokenizeInput): Promise { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.client.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_DETOKENIZE_INPUT, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + + validateDetokenizeInput(detokenizeInput); + this.emitPureJsRequest(resolve, reject, PUREJS_TYPES.DETOKENIZE, { + records: detokenizeInput.records, + }); + } catch (e:any) { + printLog(e.message, MessageType.ERROR, this.context.logLevel); + reject(e); + } + }); + } + + insert(records: IInsertRecordInput, options?:IInsertOptions): Promise { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.client.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, + this.context.logLevel); + if (options) { + options = { ...options, tokens: options?.tokens !== undefined ? options.tokens : true }; + } else { + options = { + tokens: true, + }; + } + if (options?.upsert) { + validateUpsertOptions(options.upsert); + } + validateInsertRecords(records, options); + this.emitPureJsRequest(resolve, reject, PUREJS_TYPES.INSERT, { + records, + options, + }, true); + } catch (e:any) { + printLog(e.message, MessageType.ERROR, this.context.logLevel); + reject(e); + } + }); + } + + update(record: IUpdateRequest, options?: IUpdateOptions): Promise { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.client.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, + this.context.logLevel); + + validateUpdateRecord(record, options); + this.emitPureJsRequest(resolve, reject, PUREJS_TYPES.UPDATE, { + record, + options, + }, true); + } catch (e: any) { + printLog(e.message, MessageType.ERROR, this.context.logLevel); + reject(e); + } + }); + } + + getById(getByIdInput: IGetByIdInput): Promise { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.client.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_GET_BY_ID_INPUT, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + + validateGetByIdInput(getByIdInput); + this.emitPureJsRequest(resolve, reject, PUREJS_TYPES.GET_BY_SKYFLOWID, { + records: getByIdInput.records, + }); + } catch (e:any) { + printLog(e.message, MessageType.ERROR, this.context.logLevel); + reject(e); + } + }); + } + + get(getInput: IGetInput, options?: IGetOptions): Promise { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.client.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_GET_INPUT, CLASS_NAME), + MessageType.LOG, + this.context.logLevel); + + validateGetInput(getInput, options); + this.emitPureJsRequest(resolve, reject, PUREJS_TYPES.GET, { + records: getInput.records, + options, + }); + } catch (e:any) { + printLog(e.message, MessageType.ERROR, this.context.logLevel); + reject(e); + } + }); + } + + delete(records: IDeleteRecordInput, options?: IDeleteOptions): Promise { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.client.config); + printLog( + parameterizedString(logs.infoLogs.VALIDATE_DELETE_INPUT, CLASS_NAME), MessageType.LOG, + this.context.logLevel, + ); + + validateDeleteRecords(records, options); + this.emitPureJsRequest(resolve, reject, PUREJS_TYPES.DELETE, { + records, + options, + }, true); + } catch (e:any) { + printLog(e.message, MessageType.ERROR, this.context.logLevel); + reject(e); + } + }); + } +} +export default SkyflowContainer; diff --git a/src/core/external/threeds/threeds.ts b/packages/skyflow-js/src/external/threeds/threeds.ts similarity index 97% rename from src/core/external/threeds/threeds.ts rename to packages/skyflow-js/src/external/threeds/threeds.ts index 2d0362121..963a925e3 100644 --- a/src/core/external/threeds/threeds.ts +++ b/packages/skyflow-js/src/external/threeds/threeds.ts @@ -1,5 +1,5 @@ -import SkyflowError from '../../../libs/skyflow-error'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import SkyflowError from '@core/errors'; /* Copyright (c) 2022 Skyflow, Inc. diff --git a/src/index-internal.ts b/packages/skyflow-js/src/index-internal.ts similarity index 71% rename from src/index-internal.ts rename to packages/skyflow-js/src/index-internal.ts index 1cf7049fa..4cb246e3b 100644 --- a/src/index-internal.ts +++ b/packages/skyflow-js/src/index-internal.ts @@ -2,15 +2,16 @@ Copyright (c) 2022 Skyflow, Inc. */ import 'core-js/stable'; -import RevealFrame from './core/internal/reveal/reveal-frame'; import { COMPOSABLE_REVEAL, FRAME_ELEMENT, FRAME_REVEAL, SKYFLOW_FRAME_CONTROLLER, -} from './core/constants'; -import SkyflowFrameController from './core/internal/skyflow-frame/skyflow-frame-controller'; -import logs from './utils/logs'; +} from '@core/constants'; +import logs from '@core/utils/logs'; +import RevealComposableFrameElementInit from './internal/composable-frame-element-init'; +import RevealFrame from './internal/reveal/reveal-frame'; +import SkyflowFrameController from './internal/skyflow-frame/skyflow-frame-controller'; import { MessageType, LogLevel } from './utils/common'; import { printLog, @@ -18,8 +19,12 @@ import { getElementName, } from './utils/logs-helper'; import { getAtobValue, getValueFromName } from './utils/helpers'; -import FrameElementInit from './core/internal/frame-element-init'; -import RevealComposableFrameElementInit from './core/internal/composable-frame-element-init'; +import FrameElementInit from './internal/frame-element-init'; + +// The composable reveal frame's variant seam (reveal transport + the DOM-heavy +// RevealFrame) is bound in the package subclass RevealComposableFrameElementInit +// above — no runtime variant registry. This entry runs only in the iframe +// bundle, so RevealFrame stays out of the main-thread browser/node bundles. (function init(root: any) { try { diff --git a/packages/skyflow-js/src/index-node.ts b/packages/skyflow-js/src/index-node.ts new file mode 100644 index 000000000..70b34264e --- /dev/null +++ b/packages/skyflow-js/src/index-node.ts @@ -0,0 +1,102 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import CoreCollectElement from '@core/external/collect/collect-element'; +import CoreComposableElement from './external/collect/compose-collect-element'; +import type { CollectElementUpdateOptions } from './utils/common'; +import Skyflow from './skyflow'; + +export { + RedactionType, + ValidationRuleType, + EventName, + LogLevel, + Env, + ErrorType, +} from './utils/common'; + +export type { + IInsertRecordInput as InsertRequest, + IInsertRecord as InsertRecord, + IInsertOptions as InsertOptions, + IUpdateRequest as UpdateRequest, + IUpdateOptions as UpdateOptions, + UpdateResponse, + InsertResponse, + IDetokenizeInput as DetokenizeRequest, + DetokenizeRecord, + DetokenizeResponse, + IDeleteRecordInput as DeleteRequest, + IDeleteRecord as DeleteRecord, + IDeleteOptions as DeleteOptions, + DeleteResponse, + IGetInput as GetRequest, + IGetRecord as GetRecord, + IGetOptions as GetOptions, + GetResponse, + IGetByIdInput as GetByIdRequest, + GetByIdResponse, + ContainerOptions, + CollectElementInput, + CollectElementUpdateOptions, + CollectElementOptions, + ICollectOptions as CollectOptions, + CollectResponse, + UploadFilesResponse, + CardMetadata, + InputStyles, + LabelStyles, + ErrorTextStyles, + IRevealRecord as RevealRecord, + RevealResponse, + RenderFileResponse, + IValidationRule as ValidationRule, + ElementState, + ErrorMessages, +} from './utils/common'; + +export type { + IRevealElementInput as RevealElementInput, + IRevealElementOptions as RevealElementOptions, +} from './external/reveal/reveal-container'; + +export type { ThreeDSBrowserDetails } from './external/threeds/threeds'; + +export { + CardType, +} from '@core/constants'; + +// privacyDB's public ElementType (base + file elements) is defined in ./utils/common, +// not @core (which only holds the internal BaseElementType / FileElementType). +export { ElementType } from './utils/common'; + +export { + ContainerType, +} from './skyflow'; + +export type { + ISkyflow as SkyflowConfig, +} from './skyflow'; + +// The @core element classes are generic over their update-options type, +// defaulting to the identity-neutral base (no `table`/`skyflowID`). Bind them to +// privacyDB's CollectElementUpdateOptions so the published `update()` accepts +// `{ table, skyflowID }` — matching the pre-split (2.7.9) surface. The runtime +// value stays the real @core class (so `instanceof` is preserved); only the +// exported TYPE is parameterized. The value/type pair below shares one name +// across the value and type namespaces (legal in TS; no-redeclare can't tell). +export const CollectElement = CoreCollectElement; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type CollectElement = CoreCollectElement; +export const ComposableElement = CoreComposableElement; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ComposableElement = CoreComposableElement; + +export { default as CollectContainer } from './external/collect/collect-container'; +export { default as ComposableContainer } from './external/collect/compose-collect-container'; +export { default as RevealContainer } from './external/reveal/reveal-container'; +export { default as RevealElement } from './external/reveal/reveal-element'; +export { default as ThreeDS } from './external/threeds/threeds'; +export { default as ComposableRevealContainer } from './external/reveal/composable-reveal-container'; +export { default as ComposableRevealElement } from './external/reveal/composable-reveal-element'; +export default Skyflow; diff --git a/src/index.ts b/packages/skyflow-js/src/index.ts similarity index 100% rename from src/index.ts rename to packages/skyflow-js/src/index.ts diff --git a/packages/skyflow-js/src/internal/composable-frame-element-init.ts b/packages/skyflow-js/src/internal/composable-frame-element-init.ts new file mode 100644 index 000000000..a4cfd0203 --- /dev/null +++ b/packages/skyflow-js/src/internal/composable-frame-element-init.ts @@ -0,0 +1,49 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB composable reveal-frame init: the shared @core base bound to +// privacyDB's reveal transport (api-utils/reveal `*Composable` mappers) and its +// DOM-heavy `RevealFrame`. Instantiated only from src/index-internal (iframe +// bundle) via the static factory, so `RevealFrame` stays out of the main-thread +// browser/node bundles. +import CoreRevealComposableFrameElementInit from '@core/internal/composable-frame-element-init'; +import { Context, IRevealRecordComposable, IRevealResponseType } from '@core/types'; +import { + fetchRecordsByTokenIdComposable, + formatRecordsForClientComposable, +} from '../api-utils/reveal'; +import RevealFrame from './reveal/reveal-frame'; + +class RevealComposableFrameElementInit extends CoreRevealComposableFrameElementInit { + private static frameEle?: RevealComposableFrameElementInit; + + static startFrameElement = () => { + RevealComposableFrameElementInit.frameEle = new RevealComposableFrameElementInit(); + }; + + // eslint-disable-next-line class-methods-use-this + protected fetchRecordsByTokenIdComposable( + tokenIdRecords: IRevealRecordComposable[], + client: any, + authToken: string, + ): Promise { + return fetchRecordsByTokenIdComposable(tokenIdRecords, client, authToken); + } + + // eslint-disable-next-line class-methods-use-this + protected formatRecordsForClientComposable(response: any): Record { + return formatRecordsForClientComposable(response); + } + + // eslint-disable-next-line class-methods-use-this + protected createRevealFrame( + record: any, + context: Context, + containerId: string, + rootDiv?: HTMLDivElement, + ) { + return new RevealFrame(record, context, containerId, rootDiv); + } +} + +export default RevealComposableFrameElementInit; diff --git a/packages/skyflow-js/src/internal/frame-element-init.ts b/packages/skyflow-js/src/internal/frame-element-init.ts new file mode 100644 index 000000000..4bdbe3b87 --- /dev/null +++ b/packages/skyflow-js/src/internal/frame-element-init.ts @@ -0,0 +1,539 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB composable-collect controller-frame init: the shared @core base +// (validation, request-object assembly, DOM/grid build, message handling) plus the +// injected divergence — dispatchCollectRequest() (builds the privacyDB v1 insert +// record request, dispatches insert + update-by-skyflowID, aggregates records/errors) +// and the privacyDB-only file-upload surface: the two file-message hooks +// (handleMultiFileMessages / handleFileUploadRequest) and their upload machinery. +import Client from '@core/client'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import { + COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, +} from '@core/constants'; +import SkyflowError from '@core/errors'; +import IFrameFormElement from '@core/internal/iframe-form'; +import { constructElementsInsertReq } from '@core/api-utils/collect'; +import { + CVVMap, ErrorType, MessageType, +} from '@core/types'; +import CoreFrameElementInit from '@core/internal/frame-element-init'; +import { + fileValidation, generateUploadFileName, vaildateFileName, +} from '../utils/helpers'; +import { + constructInsertRecordRequest, insertDataInCollect, + updateRecordsBySkyflowIDComposable, +} from '../api-utils/collect'; +import { printLog } from '../utils/logs-helper'; + +export default class FrameElementInit extends CoreFrameElementInit { + private static frameEle?: FrameElementInit; + + // privacyDB per-request client used by the file-upload machinery (independent of + // the base's handshake client, which the request tails never read). + #client!: Client; + + static startFrameElement = () => { + FrameElementInit.frameEle = new FrameElementInit(); + }; + + // eslint-disable-next-line class-methods-use-this + protected dispatchCollectRequest( + insertRequestObject: any, + updateRequestObject: any, + _cvvMap: CVVMap, + options: any, + clientConfig: any, + errorMessages?: Record, + ): Promise { + let finalInsertRequest; + let finalInsertRecords; + let finalUpdateRecords; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertRequestObject, updateRequestObject, options, + ); + finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + const client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + if (errorMessages && client) { + client.setErrorMessages(errorMessages); + } + const sendRequest = () => new Promise((rootResolve, rootReject) => { + const insertPromiseSet: Promise[] = []; + + if (finalInsertRequest.length !== 0) { + insertPromiseSet.push( + insertDataInCollect(finalInsertRequest, + client, options, finalInsertRecords, clientConfig.authToken as string), + ); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + insertPromiseSet.push( + updateRecordsBySkyflowIDComposable( + finalUpdateRecords, client, options, clientConfig.authToken as string, + ), + ); + } + if (insertPromiseSet.length !== 0) { + Promise.allSettled(insertPromiseSet).then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + + resultSet.forEach((result: + { status: string; value: any; reason?: any; }) => { + if (result.status === 'fulfilled') { + if (result.value.records !== undefined && Array.isArray(result.value.records)) { + result.value.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.value.errors !== undefined && Array.isArray(result.value.errors)) { + result.value.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } else { + if (result.reason?.records !== undefined && Array.isArray(result.reason?.records)) { + result.reason.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.reason?.errors !== undefined && Array.isArray(result.reason?.errors)) { + result.reason.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } + }); + if (errorsResponse.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse.length === 0) rootReject({ errors: errorsResponse }); + else rootReject({ records: recordsResponse, errors: errorsResponse }); + }); + } + }); + + return sendRequest(); + } + + // privacyDB per-element multi-file upload messages (MULTIPLE_UPLOAD_FILES). + protected handleMultiFileMessages(event: MessageEvent): void { + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement.fieldType + === ELEMENTS.MULTI_FILE_INPUT.name) { + if (event?.data && event?.data?.name === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${inputElement.iFrameName}`) { + this.#client = Client.fromJSON(event?.data?.clientConfig); + this.multipleUploadFiles(inputElement, event?.data?.clientConfig, + event?.data?.options, event?.data?.errorMessages) + ?.then((response: any) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: response, + }, this.clientMetaData?.clientDomain); + }).catch((error) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } + } + } + }); + } + + // privacyDB bulk file-upload request (COLLECT_TYPES.FILE_UPLOAD). + protected handleFileUploadRequest(event: MessageEvent): void { + if (event?.data?.data && event?.data?.data?.type === COLLECT_TYPES.FILE_UPLOAD) { + this.parallelUploadFiles(event.data.data, + event.data.clientConfig, event?.data?.errorMessages) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData?.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } + } + + private parallelUploadFiles = (options, config, + errorMessages?: Record) => new Promise((rootResolve, rootReject) => { + const promises: Promise[] = []; + this.iframeFormList.forEach((inputElement) => { + let res: Promise; + if (inputElement) { + if ( + inputElement.fieldType + === ELEMENTS.FILE_INPUT.name + ) { + res = this.uploadFiles(inputElement, config, errorMessages); + promises.push(res); + } + } + }); + if (promises.length === 0) { + rootReject(new SkyflowError(SKYFLOW_ERROR_CODE.NO_FILE_ELEMENT_FOUND, [], true)); + } + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response); + } + } + } else if (result.status === 'rejected') { + if (result.reason?.error) { + errorResponse.push({ error: result.reason?.error }); + } else { + errorResponse.push(result.reason); + } + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }); + + uploadFiles = (fileElement, clientConfig, errorMessages?: Record) => { + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + if (errorMessages && this.#client) { + this.#client.setErrorMessages(errorMessages); + } + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const fileUploadObject: any = {}; + + const { + state, tableName, skyflowID, onFocusChange, preserveFileName, + } = fileElement; + + if (state.isRequired) { + onFocusChange(false); + } + try { + fileValidation(state.value, state.isRequired, fileElement); + } catch (err) { + return Promise.reject(err); + } + + const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); + + if (!validatedFileState) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); + } + fileUploadObject[state.name] = state.value; + + const formData = new FormData(); + + const column = Object.keys(fileUploadObject)[0]; + + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + + formData.append('columnName', column); + formData.append('tableName', tableName); + + if (preserveFileName) { + const isValidFileName = vaildateFileName(state.value.name); + if (!isValidFileName) { + return Promise.reject( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), + ); + } + formData.append('file', value); + } else { + const generatedFileName = generateUploadFileName(state.value.name); + formData.append('file', new File([value], generatedFileName, { type: state.value.type })); + } + + if (skyflowID) { + formData.append('skyflowID', skyflowID); + } + + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response: any) => { + rootResolve(response); + }) + .catch((error) => { + if (error?.error) { + rootReject({ + error: { + code: error?.error?.code, + description: error?.error?.description, + type: error?.error?.type, + }, + }); + } + rootReject(error); + }); + }); + + return sendRequest(); + }; + + // eslint-disable-next-line consistent-return + private multipleUploadFiles = + (fileElement: IFrameFormElement, + clientConfig, metaData, + errorMessages?: Record) => new Promise((rootResolve, rootReject) => { + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + if (errorMessages && this.#client) { + this.#client.setErrorMessages(errorMessages); + } + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + + const { + state, tableName, onFocusChange, preserveFileName, + } = fileElement; + if (state.isRequired) { + onFocusChange(false); + } + + if (state.value === undefined || state.value === null || state.value === '') { + rootReject({ error: 'No files selected' }); + return; + } + + const files = state.value instanceof FileList ? Array.from(state.value) : [state.value]; + try { + this.validateFiles(files, state, fileElement); + } catch (err: any) { + rootReject({ errorResponse: [{ error: err?.error || err?.errors?.[0] || err }] }); + return; + } + + const uploadFile = (file: File, skyflowID?: string) => { + const formData = new FormData(); + formData.append('columnName', state.name); + if (tableName) formData.append('tableName', tableName); + if (preserveFileName) { + formData.append('file', file); + } else { + const generatedFileName = generateUploadFileName(file.name); + formData.append('file', new File([file], generatedFileName, { type: file.type })); + } + if (skyflowID) formData.append('skyflowID', skyflowID); + const client = this.#client; + return this.#client.request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${this.#client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }); + }; + + if (metaData && Object.keys(metaData).length > 0) { + const insertRequest = this.createInsertRequest(files.length, metaData); + this.insertDataCallInMultiFiles( + insertRequest, this.#client, tableName as string, clientConfig.authToken as string, + ).then((response: any) => { + const skyflowIDs = this.extractSkyflowIDs(response); + if (skyflowIDs.length === 0) { + rootReject({ error: 'No skyflow IDs returned from insert data' }); + return; + } + const promises = files.map((file, idx) => uploadFile(file, skyflowIDs[idx])); + Promise.allSettled(promises).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + if (result?.reason?.error) { + errorResponse.push({ error: result?.reason?.error }); + } else { + errorResponse.push({ error: result.reason }); + } + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }).catch((error) => { + printLog(`${error}`, MessageType.LOG, this.context?.logLevel); + rootReject({ + error: error?.error || error, + }); + }); + } else { + const promises = files.map((file) => uploadFile(file)); + Promise.allSettled(promises).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + if (result?.reason?.error) { + errorResponse.push({ error: result?.reason?.error }); + } else { + errorResponse.push({ error: result.reason }); + } + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + } + }); + + private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { + if (files.length > fileElement.maxFileCount) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.FILE_COUNT_EXCEEDED, + [String(fileElement.maxFileCount)], + true, + ); + } + files.forEach((file) => { + const validatedFileState = fileValidation(file, state.isRequired, fileElement); + if (!validatedFileState) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); + } + + const isValidFileName = vaildateFileName(file.name); + if (!isValidFileName) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); + } + }); + return true; + }; + + // eslint-disable-next-line class-methods-use-this + private createInsertRequest = (numberOfRequests: number, options = {}) => { + // Create basic request structure + const request = { + records: [] as Array<{ fields: Record }>, + tokenization: false, + }; + + // Add empty field objects based on number of requests + for (let i = 0; i < numberOfRequests; i += 1) { + request.records.push({ + fields: options === undefined ? {} : options, + }); + } + + return request; + }; + + // eslint-disable-next-line class-methods-use-this + private extractSkyflowIDs = (response: { records: Array<{ skyflow_id: string }> }): string[] => { + if (!response?.records || !Array.isArray(response.records)) { + return []; + } + + return response.records + .map((record) => record.skyflow_id) + .filter((id) => id !== undefined && id !== null); + }; + + private insertDataCallInMultiFiles = ( + insertRequest, + client: Client, + tableName: string, + authToken: string, + ) => new Promise((rootResolve, rootReject) => { + client + .request({ + body: JSON.stringify(insertRequest), + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + .then((response: any) => { + // Extract skyflow IDs from response + const skyflowIDs = this.extractSkyflowIDs(response); + rootResolve({ + ...response, + skyflowIDs, // Add extracted IDs to response + }); + }) + .catch((error) => { + if (error?.error) { + rootReject({ + error: { + code: error?.error?.code, + description: error?.error?.description, + type: error?.error?.type, + }, + }); + } else { + rootReject(error); + } + }); + }); +} diff --git a/packages/skyflow-js/src/internal/internal-types/index.ts b/packages/skyflow-js/src/internal/internal-types/index.ts new file mode 100644 index 000000000..a8499b816 --- /dev/null +++ b/packages/skyflow-js/src/internal/internal-types/index.ts @@ -0,0 +1,48 @@ +import { ContainerType, ElementInfo, ClientMetadata } from '@core/types'; +import { AnyElementType } from '@core/constants'; +import { ClientToJSON } from '@core/client'; +import CollectContainer from '../../external/collect/collect-container'; +import ComposableContainer from '../../external/collect/compose-collect-container'; +import RevealContainer from '../../external/reveal/reveal-container'; +import { ICollectOptions } from '../../utils/common'; +import SkyflowContainer from '../../external/skyflow-container'; + +// The variant-neutral internal types now live in `@core/types`; re-exported here +// so the existing `internal-types` importers keep resolving them. The types +// below stay in the package because they reference privacyDB container classes, +// the client, or the privacyDB `ICollectOptions`. +export type { + ElementInfo, + ContainerProps, + RevealContainerProps, + InternalState, + BatchInsertRequestBody, + FormattedCollectElementOptions, + ClientMetadata, +} from '@core/types'; + +export interface TokenizeDataInput extends ICollectOptions{ + type: string; + elementIds: Array; + containerId: string; +} + +export interface UploadFileDataInput extends ICollectOptions { + type: string; + elementIds: Array; + containerId: string; +} + +export interface SkyflowElementProps { + id: string; + type: AnyElementType; + element: HTMLElement; + container: CollectContainer | RevealContainer | ComposableContainer; +} + +export interface Metadata extends ClientMetadata { + clientJSON: ClientToJSON; + containerType: ContainerType; + skyflowContainer: SkyflowContainer; + getSkyflowBearerToken: () => Promise; +} diff --git a/packages/skyflow-js/src/internal/reveal/reveal-frame.ts b/packages/skyflow-js/src/internal/reveal/reveal-frame.ts new file mode 100644 index 000000000..9060919b0 --- /dev/null +++ b/packages/skyflow-js/src/internal/reveal/reveal-frame.ts @@ -0,0 +1,250 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB reveal-frame: the shared @core reveal-frame base plus the file-render +// feature. The base's two protected hooks (registerRenderFileResponseListener / +// handleRevealCallRequest) are overridden here to wire the RENDER_FILE bus + +// message paths, so this DOM/file-upload-adjacent surface stays out of flowDB's +// token-only bundle. +import bus from 'framebus'; +import getCssClassesFromJss from '@core/libs/jss-styles'; +import { + ELEMENT_EVENTS_TO_IFRAME, + STYLE_TYPE, + RENDER_ELEMENT_IMAGE_STYLES, + DEFAULT_FILE_RENDER_ERROR, + ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, +} from '@core/constants'; +import properties from '@core/properties'; +import Client from '@core/client'; +import CoreRevealFrame from '@core/internal/reveal/reveal-frame'; +import { + IRenderResponseType, IRevealRecord, +} from '../../utils/common'; +import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../api-utils/reveal'; + +const { getType } = require('mime'); + +class RevealFrame extends CoreRevealFrame { + #client!: Client; + + protected registerRenderFileResponseListener(): void { + // Deferred wrapper (not `this.sub2` directly): the @core base constructor + // calls this during super(), before this subclass's `sub2` arrow-field is + // initialized — so the lookup must happen at fire-time, not registration. + bus + .target(window.location.origin) + .on( + ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.name, + (responseUrl) => this.sub2(responseUrl), + ); + } + + protected handleRevealCallRequest(event: MessageEvent): void { + if (event?.origin === this.clientDomain) { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.name) { + if (event?.data?.data?.iframeName === this.name + && event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + this.renderFile(this.record, event?.data?.clientConfig, + event?.data?.errorMessages)?.then((resolvedResult) => { + const result = formatForRenderClient( + resolvedResult as IRenderResponseType, + this.record?.column, + ); + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result, + }, + }, this.clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + })?.catch((error) => { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result: { + errors: error, + }, + }, + }, this.clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + }); + } + } + } + } + + private sub2 = (responseUrl: { iframeName?: string; error?: string; url?: string }) => { + if (responseUrl.iframeName === this.name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.record, 'altText')) { + this.dataElememt.innerText = this.record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.name, + { + height: this.elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url as string); + this.addFileRender(responseUrl.url as string, ext); + } + } + }; + + private renderFile(data: IRevealRecord, clientConfig, customErrorMessages): + Promise | undefined { + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); + this.#client.setErrorMessages(customErrorMessages ?? {}); + return new Promise((resolve, reject) => { + try { + getFileURLFromVaultBySkyflowIDComposable(data, this.#client, clientConfig.authToken) + .then((resolvedResult) => { + let url = ''; + if (resolvedResult.fields && data.column) { + url = resolvedResult.fields[data.column]; + } + this.sub2({ + url, + iframeName: this.name, + }); + resolve(resolvedResult); + }, + (rejectedResult) => { + this.sub2({ + error: DEFAULT_FILE_RENDER_ERROR, + iframeName: this.name, + }); + reject(rejectedResult); + }); + } catch (err) { + reject(err); + } + }); + } + + // eslint-disable-next-line class-methods-use-this + private getExtension(url: string) { + try { + const params = new URL(url).searchParams; + const name = params.get('response-content-disposition'); + if (name) { + const ext = getType(name); + return ext; + } + return ''; + } catch { + return ''; + } + } + + private addFileRender(responseUrl: string, ext: string) { + let tag = ''; + if (typeof ext === 'string' && ext.includes('image')) { + tag = 'img'; + } else { + tag = 'embed'; + } + const fileElement = document.createElement(tag); + fileElement.addEventListener('load', () => { + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.name, + { + height: this.elementContainer.scrollHeight, + }, () => { + }, + ); + }); + fileElement.className = `SkyflowElement-${tag}-${STYLE_TYPE.BASE}`; + if (tag === 'embed' && typeof ext === 'string') { + fileElement.setAttribute('type', ext); + } + fileElement.setAttribute('src', responseUrl); + if (Object.prototype.hasOwnProperty.call(this.record, 'inputStyles')) { + this.inputStyles = {}; + if (tag === 'img') { + this.inputStyles[STYLE_TYPE.BASE] = { + ...this.record.inputStyles[STYLE_TYPE.BASE], + }; + if (this.record?.inputStyles + && this.record?.inputStyles[STYLE_TYPE.BASE] + && this.record?.inputStyles[STYLE_TYPE.BASE]?.overflow && this.composableContainer) { + this.elementContainer.className = `SkyflowElement-div-container-${STYLE_TYPE.BASE}`; + const divStyles = { + [STYLE_TYPE.BASE]: { + ...this.record.inputStyles[STYLE_TYPE.BASE], + }, + }; + this.elementContainer.style.overflow = this.record + .inputStyles[STYLE_TYPE.BASE].overflow as string; + this.inputStyles[STYLE_TYPE.BASE] = { + ...this.inputStyles[STYLE_TYPE.BASE], + }; + getCssClassesFromJss(divStyles, 'div-container'); + } else { + this.inputStyles[STYLE_TYPE.BASE] = { + ...RENDER_ELEMENT_IMAGE_STYLES[STYLE_TYPE.BASE], + ...this.inputStyles[STYLE_TYPE.BASE], + }; + getCssClassesFromJss(this.inputStyles, tag); + } + } else { + this.inputStyles[STYLE_TYPE.BASE] = { + ...RENDER_ELEMENT_IMAGE_STYLES[STYLE_TYPE.BASE], + ...this.record.inputStyles[STYLE_TYPE.BASE], + }; + getCssClassesFromJss(this.inputStyles, tag); + } + } + + if (this.elementContainer.childNodes[0] !== undefined) { + this.elementContainer.innerHTML = ''; + this.elementContainer.appendChild(fileElement); + } else { + this.elementContainer.appendChild(fileElement); + } + if (fileElement instanceof HTMLImageElement + && this.record?.inputStyles + && this.record?.inputStyles[STYLE_TYPE.BASE] + && this.record?.inputStyles[STYLE_TYPE.BASE]?.overflow && this.composableContainer) { + fileElement.onload = () => { + if (fileElement?.naturalWidth && fileElement?.naturalHeight) { + fileElement.style.width = `${fileElement.naturalWidth}px`; + fileElement.style.height = `${fileElement.naturalHeight}px`; + } + + if (this.record?.inputStyles[STYLE_TYPE.BASE]?.width) { + this.elementContainer.style.width = this.record.inputStyles[STYLE_TYPE.BASE].width; + } + if (this.record?.inputStyles[STYLE_TYPE.BASE]?.height) { + this.elementContainer.style.height = this.record.inputStyles[STYLE_TYPE.BASE].height; + } + this.elementContainer.style.overflow = this.record + .inputStyles[STYLE_TYPE.BASE].overflow as string; + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + }; + } + } +} + +export default RevealFrame; diff --git a/packages/skyflow-js/src/internal/skyflow-frame/skyflow-frame-controller.ts b/packages/skyflow-js/src/internal/skyflow-frame/skyflow-frame-controller.ts new file mode 100644 index 000000000..ca473e1c0 --- /dev/null +++ b/packages/skyflow-js/src/internal/skyflow-frame/skyflow-frame-controller.ts @@ -0,0 +1,621 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB skyflow-frame controller. Extends the shared @core base +// (CoreSkyflowFrameController) and supplies only the privacyDB divergence: +// - registerDataAccessListeners(): the pure-JS channel +// (DETOKENIZE/INSERT/UPDATE/GET/GET_BY_SKYFLOWID/DELETE) + PUREJS_FRAME_READY +// - handleExtraCollectRequest(): file upload (COLLECT_TYPES.FILE_UPLOAD) +// - handleExtraRevealRequest(): file render (REVEAL_TYPES.RENDER_FILE) +// - sendCollectRequest(): the v1 insert + updateBySkyflowID send path +// - fetchRevealRecords()/formatRevealForClient(): the privacyDB reveal path +// - getSdkNameAndVersion()/wrapCallbackError(): telemetry + error-envelope +// The common bus topology, handshake and tokenize()/revealData() skeleton live +// in the @core base. +import bus from 'framebus'; +import { getAccessToken } from '@core/utils/bus-events'; +import { + COLLECT_TYPES, + DEFAULT_FILE_RENDER_ERROR, + ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, PUREJS_TYPES, REVEAL_TYPES, +} from '@core/constants'; +import logs from '@core/utils/logs'; +import properties from '@core/properties'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import { constructElementsInsertReq } from '@core/api-utils/collect'; +import { ICollectedElementsData } from '@core/internal/skyflow-frame/collect-elements'; +import CoreSkyflowFrameController from '@core/internal/skyflow-frame/skyflow-frame-controller-base'; +import { injectCoralogixTrackingScript } from '@core/helpers'; +import SkyflowError from '@core/errors'; +import IFrameFormElement from '@core/internal/iframe-form'; +import { SdkInfo } from '@core/client'; +import { + constructInsertRecordRequest, + constructInsertRecordResponse, + constructUpdateRecordRequest, + constructUpdateRecordResponse, + constructUploadResponse, + updateRecordsBySkyflowID, +} from '../../api-utils/collect'; +import { + fetchRecordsGET, + fetchRecordsByTokenId, + fetchRecordsBySkyflowID, + getFileURLFromVaultBySkyflowID, + formatRecordsForClient, +} from '../../api-utils/reveal'; +import { printLog, parameterizedString } from '../../utils/logs-helper'; +import { + IRevealRecord, + IGetRecord, + MessageType, + ISkyflowIdRecord, + IGetOptions, + IInsertRecordInput, + IInsertOptions, + UploadFilesResponse, + RevealResponse, + InsertResponse, + CollectResponse, + IDeleteRecordInput, + IRenderResponseType, + IUpdateRequest, + UpdateResponse, + IUpdateOptions, +} from '../../utils/common'; +import { deleteData } from '../../api-utils/delete'; +import { + fileValidation, generateUploadFileName, getSDKNameAndVersion, vaildateFileName, +} from '../../utils/helpers'; +import { + BatchInsertRequestBody, TokenizeDataInput, UploadFileDataInput, +} from '../internal-types'; + +const CLASS_NAME = 'SkyflowFrameController'; +class SkyflowFrameController + extends CoreSkyflowFrameController { + // privacyDB registers the pure-JS data-access channel, so it announces those + // listeners in the readiness handshake (see registerDataAccessListeners). + protected registersDataAccessListeners = true; + + static init(clientId: string): SkyflowFrameController { + injectCoralogixTrackingScript(); + return new SkyflowFrameController(clientId); + } + + // privacyDB telemetry identity (deliberate loose-coupling duplication: each + // package owns its telemetry helper). + // eslint-disable-next-line class-methods-use-this + protected getSdkNameAndVersion(metaData?: string): SdkInfo { + return getSDKNameAndVersion(metaData); + } + + // privacyDB error envelope for collect/reveal callbacks. + // eslint-disable-next-line class-methods-use-this + protected wrapCallbackError(error: any): any { + return { error }; + } + + protected fetchRevealRecords(revealRecords: IRevealRecord[]): Promise { + return fetchRecordsByTokenId(revealRecords, this.client, false); + } + + // eslint-disable-next-line class-methods-use-this + protected formatRevealForClient(result: any): RevealResponse { + return formatRecordsForClient(result); + } + + // privacyDB pure-JS data channel: DETOKENIZE/INSERT/UPDATE/GET/GET_BY_SKYFLOWID/ + // DELETE, plus the PUREJS_FRAME_READY handshake. flowDB is elements-only and + // does not register this channel. + protected registerDataAccessListeners(): void { + bus + .target(this.clientDomain) + .on( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.clientId, + (data, callback) => { + printLog( + parameterizedString( + logs.infoLogs.CAPTURE_PURE_JS_REQUEST, + CLASS_NAME, + data.type, + ), + MessageType.LOG, + this.context.logLevel, + ); + + // Every pure-JS op shares one shape: run the request, then log + callback + // the result on success and log + callback({ error }) on failure. Only the + // request and its two log lines differ per type, so they live in this + // table and the single handler below dispatches on data.type. + const dispatch: Record Promise; + successLog: string; + errorLog: string; + }> = { + [PUREJS_TYPES.DETOKENIZE]: { + op: () => fetchRecordsByTokenId(data.records as IRevealRecord[], this.client, true), + successLog: logs.infoLogs.FETCH_RECORDS_RESOLVED, + errorLog: logs.errorLogs.FETCH_RECORDS_REJECTED, + }, + [PUREJS_TYPES.INSERT]: { + op: () => this.insertData( + data.records as IInsertRecordInput, data.options as IInsertOptions, + ), + successLog: logs.infoLogs.INSERT_RECORDS_RESOLVED, + errorLog: logs.errorLogs.INSERT_RECORDS_REJECTED, + }, + [PUREJS_TYPES.UPDATE]: { + op: () => this.updateData( + data.record as IUpdateRequest, data.options as IUpdateOptions, + ), + successLog: logs.infoLogs.UPDATE_RECORD_RESOLVED, + errorLog: logs.errorLogs.UPDATE_RECORD_REJECTED, + }, + [PUREJS_TYPES.GET]: { + op: () => fetchRecordsGET( + data.records as IGetRecord[], this.client, data.options as IGetOptions, + ), + successLog: logs.infoLogs.GET_RESOLVED, + errorLog: logs.errorLogs.GET_REJECTED, + }, + [PUREJS_TYPES.GET_BY_SKYFLOWID]: { + op: () => fetchRecordsBySkyflowID(data.records as ISkyflowIdRecord[], this.client), + successLog: logs.infoLogs.GET_BY_SKYFLOWID_RESOLVED, + errorLog: logs.errorLogs.GET_BY_SKYFLOWID_REJECTED, + }, + [PUREJS_TYPES.DELETE]: { + op: () => deleteData( + data.records as IDeleteRecordInput, data.options || {}, this.client, + ), + successLog: logs.infoLogs.DELETE_RESOLVED, + errorLog: logs.errorLogs.DELETE_RECORDS_REJECTED, + }, + }; + + const entry = dispatch[data.type as string]; + if (!entry) return; + entry.op().then( + (resolvedResult: any) => { + printLog( + parameterizedString(entry.successLog, CLASS_NAME), + MessageType.LOG, + this.context.logLevel, + ); + callback(resolvedResult); + }, + (rejectedResult: any) => { + printLog( + parameterizedString(entry.errorLog), + MessageType.ERROR, + this.context.logLevel, + ); + callback({ error: rejectedResult }); + }, + ); + }, + ); + } + + // privacyDB-only collect extra: file upload. + protected handleExtraCollectRequest(data: any, callback: (response: any) => void): void { + if (data.type === COLLECT_TYPES.FILE_UPLOAD) { + printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), + MessageType.LOG, this.context.logLevel); + const uploadFilesDataInput = { + ...data, + type: data.type, + elementIds: data.elementIds as string[], + containerId: data.containerId as string, + }; + this.parallelUploadFiles(uploadFilesDataInput) + .then((response: UploadFilesResponse) => { + callback(response); + }) + .catch((error: UploadFilesResponse) => { + callback({ error }); + }); + } + } + + // privacyDB-only reveal extra: file render. + protected handleExtraRevealRequest(data: any, callback: (response: any) => void): void { + if (data.type === REVEAL_TYPES.RENDER_FILE) { + printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, this.context.logLevel); + this.renderFile(data.records as IRevealRecord, data.iframeName as string).then( + (resolvedResult) => { + callback( + resolvedResult, + ); + }, + (rejectedResult) => { + callback({ errors: rejectedResult }); + }, + ); + } + } + + insertData(records: IInsertRecordInput, options: IInsertOptions): Promise { + const requestBody: Array = constructInsertRecordRequest( + records, options, + ); + return new Promise((rootResolve, rootReject) => { + getAccessToken(this.clientId).then((authToken) => { + this.client + .request({ + body: JSON.stringify({ records: requestBody }), + requestMethod: 'POST', + url: + `${this.client.config.vaultURL}/v1/vaults/${ + this.client.config.vaultID}`, + headers: { + Authorization: `Bearer ${authToken}`, + }, + + }) + .then((response: any) => { + rootResolve( + constructInsertRecordResponse( + response, + options?.tokens ?? true, + records?.records, + ), + ); + }) + .catch((error) => { + if (error?.error?.type) { + error = { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }; + } + rootReject(error); + }); + }).catch((err) => { + rootReject(err); + }); + }); + } + + updateData(updateData: IUpdateRequest, options?: IUpdateOptions): Promise { + const requestBody = constructUpdateRecordRequest( + updateData, options, + ); + return new Promise((rootResolve, rootReject) => { + getAccessToken(this.clientId).then((authToken) => { + const { table, skyflowID } = updateData; + this.client + .request({ + body: JSON.stringify(requestBody), + requestMethod: 'PUT', + url: `${this.client.config.vaultURL}/v1/vaults/${this.client.config.vaultID}/${table}/${skyflowID}`, + headers: { + Authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + .then((response: any) => { + rootResolve( + constructUpdateRecordResponse(response, options?.tokens ?? false), + ); + }) + .catch((error: any) => { + rootReject(error); + }); + }).catch((err) => { + rootReject(err); + }); + }); + } + + renderFile(data: IRevealRecord, iframeName: string): Promise { + return new Promise((resolve, reject) => { + try { + getFileURLFromVaultBySkyflowID(data, this.client) + .then((resolvedResult) => { + let url = ''; + if (resolvedResult.fields && data.column) { + url = resolvedResult.fields[data.column]; + } + bus + .target(properties.IFRAME_SECURE_SITE) + .emit( + ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + + iframeName, + { + url, + iframeName, + }, + ); + + resolve(resolvedResult); + }, + (rejectedResult) => { + bus + .target(properties.IFRAME_SECURE_SITE) + .emit( + ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + + iframeName, + { + error: DEFAULT_FILE_RENDER_ERROR, + iframeName, + }, + ); + reject(rejectedResult); + }); + } catch (err) { + reject(err); + } + }); + } + + // privacyDB collect send path: v1 batch insert + updateBySkyflowID, merging the + // insert/update responses (and their errors) into one collect response. + protected sendCollectRequest( + built: ICollectedElementsData, + options: TokenizeDataInput, + ): Promise { + const { insertResponseObject, updateResponseObject } = built; + let finalInsertRequest: Array; + let finalInsertRecords; + let finalUpdateRecords; + let insertResponse: InsertResponse; + let updateResponse: InsertResponse; + let insertErrorResponse: any; + let updateErrorResponse; + let insertDone = false; + let updateDone = false; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertResponseObject, updateResponseObject, options, + ); + finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + const client = this.client; + const sendRequest = (): Promise => new Promise((rootResolve, rootReject) => { + const clientId = client.toJSON()?.metaData?.uuid || ''; + getAccessToken(clientId).then((authToken) => { + if (finalInsertRequest.length !== 0) { + client + .request({ + body: JSON.stringify({ records: finalInsertRequest }), + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + .then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options.tokens ?? true, + finalInsertRecords.records, + ); + insertDone = true; + if (finalUpdateRecords.updateRecords.length === 0) { + rootResolve(insertResponse); + } + if (updateDone && updateErrorResponse !== undefined) { + if (updateErrorResponse.records === undefined) { + updateErrorResponse.records = insertResponse.records; + } else { + updateErrorResponse.records = (insertResponse.records || []) + .concat(updateErrorResponse.records); + } + rootReject(updateErrorResponse); + } else if (updateDone && updateResponse !== undefined) { + rootResolve( + { records: (insertResponse.records || []).concat(updateResponse.records || []) }, + ); + } + }) + .catch((error) => { + insertDone = true; + if (finalUpdateRecords.updateRecords.length === 0) { + rootReject(error); + } else { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + type: error?.error?.type, + }, + }, + ], + }; + } + if (updateDone && updateResponse !== undefined) { + const errors = insertErrorResponse.errors; + const records = updateResponse.records; + rootReject({ errors, records }); + } else if (updateDone && updateErrorResponse !== undefined) { + updateErrorResponse.errors = updateErrorResponse.errors + .concat(insertErrorResponse.errors); + rootReject(updateErrorResponse); + } + }); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + updateRecordsBySkyflowID(finalUpdateRecords, client, options) + .then((response: any) => { + updateResponse = { + records: response, + }; + updateDone = true; + if (finalInsertRequest.length === 0) { + rootResolve(updateResponse); + } + if (insertDone && insertResponse !== undefined) { + rootResolve( + { records: (insertResponse.records || []).concat(updateResponse.records || []) }, + ); + } else if (insertDone && insertErrorResponse !== undefined) { + const errors = insertErrorResponse.errors; + const records = updateResponse.records; + rootReject({ errors, records }); + } + }).catch((error) => { + updateErrorResponse = error; + updateDone = true; + if (finalInsertRequest.length === 0) { + rootReject(error); + } + if (insertDone && insertResponse !== undefined) { + if (updateErrorResponse.records === undefined) { + updateErrorResponse.records = insertResponse.records; + } else { + updateErrorResponse.records = (insertResponse.records || []) + .concat(updateErrorResponse.records); + } + rootReject(updateErrorResponse); + } else if (insertDone && insertErrorResponse !== undefined) { + updateErrorResponse.errors = updateErrorResponse.errors + .concat(insertErrorResponse.errors); + rootReject(updateErrorResponse); + } + }); + } + }).catch((err) => { + rootReject(err); + }); + }); + + return sendRequest(); + } + + parallelUploadFiles = (options: UploadFileDataInput): + Promise => new Promise((rootResolve, rootReject) => { + const id = options.containerId; + const promises: Promise[] = []; + for (let i = 0; i < options.elementIds.length; i += 1) { + let res: Promise; + const Frame = window.parent.frames[`${options.elementIds[i]}:${id}:${this.context.logLevel}:${btoa(this.clientDomain)}`]; + const inputElement = Frame.document + .getElementById(options.elementIds[i]); + if (inputElement) { + if ( + inputElement.iFrameFormElement.fieldType + === ELEMENTS.FILE_INPUT.name + ) { + res = this.uploadFiles(inputElement.iFrameFormElement); + promises.push(res); + } + } + } + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: Record[] = []; + const errorResponse: Record[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + fileUploadResponse.push(result.value); + } + } + } else if (result.status === 'rejected') { + errorResponse.push(result.reason); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }); + + uploadFiles = (fileElement: IFrameFormElement) => { + if (!this.client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const fileUploadObject: any = {}; + + const { + state, tableName, skyflowID, onFocusChange, preserveFileName, + } = fileElement; + + if (state.isRequired) { + onFocusChange(false); + } + try { + fileValidation(state.value, state.isRequired, fileElement); + } catch (err) { + return Promise.reject(err); + } + + const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); + + if (!validatedFileState) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); + } + fileUploadObject[state.name] = state.value; + + const formData = new FormData(); + + const column = Object.keys(fileUploadObject)[0]; + + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + + formData.append('columnName', column); + formData.append('tableName', tableName ?? ''); + + if (preserveFileName) { + const isValidFileName = vaildateFileName(state.value.name); + if (!isValidFileName) { + return Promise.reject( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), + ); + } + formData.append('file', value); + } else { + const generatedFileName = generateUploadFileName(state.value.name); + formData.append('file', new File([value], generatedFileName, { type: state.value.type })); + } + + if (skyflowID) { + formData.append('skyflowID', skyflowID); + } + + const client = this.client; + const sendRequest = (): + Promise => new Promise((rootResolve, rootReject) => { + const clientId = client.toJSON()?.metaData?.uuid || ''; + getAccessToken(clientId).then((authToken) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response: any) => { + rootResolve(constructUploadResponse(response)); + }) + .catch((error) => { + rootReject(error); + }); + }).catch((err) => { + rootReject(err); + }); + }); + + return sendRequest(); + }; +} +export default SkyflowFrameController; diff --git a/packages/skyflow-js/src/skyflow.ts b/packages/skyflow-js/src/skyflow.ts new file mode 100644 index 000000000..92e2c2cef --- /dev/null +++ b/packages/skyflow-js/src/skyflow.ts @@ -0,0 +1,175 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// privacyDB public Skyflow shell. The constructor, `static init()`, the +// bearer-token plumbing, the `container()` overloads/switch and the shared static +// enum getters all live in `@core/external/base-skyflow`; this subclass supplies +// only what is genuinely privacyDB-specific: +// - the five container factory hooks (which concrete class to `new`) +// - the pure-JS API surface (insert/detokenize/getById/get/delete/update), +// which flowDB does not have +// - `static get Error()` (SkyflowError) and `static get ThreeDS()` +import Client from '@core/client'; +import SkyflowError from '@core/errors'; +import BaseSkyflow from '@core/external/base-skyflow'; +import { + ContainerOptions, + ContainerType, + Context, + ICoreMetadata, + ISkyflow, + RequestMethod, + SkyflowConfigOptions, +} from '@core/types'; +import logs from '@core/utils/logs'; +import RevealContainer from './external/reveal/reveal-container'; +import CollectContainer from './external/collect/collect-container'; +import SkyflowContainer from './external/skyflow-container'; +import { printLog, parameterizedString } from './utils/logs-helper'; +import { + IInsertRecordInput, + IDetokenizeInput, + IGetInput, + MessageType, + IGetByIdInput, + IInsertOptions, + IDeleteRecordInput, + IDeleteOptions, + IGetOptions, + InsertResponse, + GetResponse, + GetByIdResponse, + DeleteResponse, + DetokenizeResponse, + IUpdateRequest, + UpdateResponse, + IUpdateOptions, + ElementType, +} from './utils/common'; +import ComposableContainer from './external/collect/compose-collect-container'; +import ThreeDS from './external/threeds/threeds'; +import ComposableRevealContainer from './external/reveal/composable-reveal-container'; + +// Relocated to @core/types (variant-neutral); re-exported here under the same +// names so `./skyflow` importers and the public surface are unchanged. +export { ContainerType }; +export type { ISkyflow, SkyflowConfigOptions }; + +const CLASS_NAME = 'Skyflow'; +class Skyflow extends BaseSkyflow< +SkyflowContainer, +CollectContainer, +RevealContainer, +ComposableContainer, +ComposableRevealContainer +> { + // ---- Injected divergence: which concrete class to `new` -------------------- + // Prototype methods, not arrow-function fields — `instantiateSkyflowContainer` + // is called from the base constructor, before subclass fields initialize. + + // eslint-disable-next-line class-methods-use-this + protected instantiateSkyflowContainer(client: Client, context: Context): SkyflowContainer { + return new SkyflowContainer(client, context); + } + + // eslint-disable-next-line class-methods-use-this + protected createCollectContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): CollectContainer { + return new CollectContainer(metaData, context, options); + } + + // eslint-disable-next-line class-methods-use-this + protected createRevealContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): RevealContainer { + return new RevealContainer(metaData, context, options); + } + + // eslint-disable-next-line class-methods-use-this + protected createComposableContainer( + metaData: ICoreMetadata, + context: Context, + options: ContainerOptions, + ): ComposableContainer { + return new ComposableContainer(metaData, context, options); + } + + // eslint-disable-next-line class-methods-use-this + protected createComposeRevealContainer( + metaData: ICoreMetadata, + context: Context, + options?: ContainerOptions, + ): ComposableRevealContainer { + return new ComposableRevealContainer(metaData, context, options); + } + + // ---- privacyDB-only pure-JS API ------------------------------------------- + + insert( + records: IInsertRecordInput, + options?: IInsertOptions, + ): Promise { + printLog(parameterizedString(logs.infoLogs.INSERT_TRIGGERED, CLASS_NAME), MessageType.LOG, + this.logLevel); + return this.skyflowContainer.insert(records, options); + } + + detokenize(detokenizeInput: IDetokenizeInput): Promise { + printLog(parameterizedString(logs.infoLogs.DETOKENIZE_TRIGGERED, CLASS_NAME), + MessageType.LOG, this.logLevel); + return this.skyflowContainer.detokenize(detokenizeInput); + } + + getById(getByIdInput: IGetByIdInput): Promise { + printLog(logs.warnLogs.GET_BY_ID_DEPRECATED, MessageType.WARN, this.logLevel); + printLog(parameterizedString(logs.infoLogs.GET_BY_ID_TRIGGERED, CLASS_NAME), + MessageType.LOG, this.logLevel); + return this.skyflowContainer.getById(getByIdInput); + } + + get(getInput: IGetInput, options?: IGetOptions): Promise { + printLog(parameterizedString(logs.infoLogs.GET_TRIGGERED, CLASS_NAME), + MessageType.LOG, this.logLevel); + return this.skyflowContainer.get(getInput, options); + } + + delete(records: IDeleteRecordInput, options?: IDeleteOptions): Promise { + printLog(parameterizedString(logs.infoLogs.DELETE_TRIGGERED, CLASS_NAME), MessageType.LOG, + this.logLevel); + return this.skyflowContainer.delete(records, options); + } + + update(record: IUpdateRequest, options?: IUpdateOptions): Promise { + printLog(parameterizedString(logs.infoLogs.UPDATE_TRIGGERED, CLASS_NAME), MessageType.LOG, + this.logLevel); + return this.skyflowContainer.update(record, options); + } + + // ---- Package-specific statics (the rest are inherited from BaseSkyflow) ---- + + static get Error() { + return SkyflowError; + } + + static get ThreeDS() { + return ThreeDS; + } + + // RequestMethod is privacyDB-only (invokeConnection / invokeGateway). Relocated + // here from BaseSkyflow so the elements-only flowDB SDK no longer inherits it. + static get RequestMethod() { + return RequestMethod; + } + + // privacyDB's ElementType (base + file elements). Overrides the removed @core + // base getter so `Skyflow.ElementType.FILE_INPUT` stays available for privacyDB. + static get ElementType() { + return ElementType; + } +} +export default Skyflow; diff --git a/packages/skyflow-js/src/utils/common/index.ts b/packages/skyflow-js/src/utils/common/index.ts new file mode 100644 index 000000000..6d9c9d4b4 --- /dev/null +++ b/packages/skyflow-js/src/utils/common/index.ts @@ -0,0 +1,66 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// The variant-neutral types now live in `@core/types`; this module re-exports +// them so the many existing `../utils/common` importers keep working, and adds +// the privacyDB-specific `ICollectOptions` (its `upsert` uses the privacyDB +// `IUpsertOptions` from api-utils/collect — the flowDB shape differs, so this +// stays in the package, not in core). +import { + ICollectOptionsBase, ICollectElementInputBase, ICollectElementOptionsBase, + ICollectElementUpdateOptionsBase, IElementStateBase, IInsertRecordInput, +} from '@core/types'; +import { BaseElementType, FileElementType } from '@core/constants'; +import { IUpsertOptions } from '../../api-utils/collect'; + +export * from '@core/types'; + +// privacyDB public element-type surface: the shared @core base PLUS the file +// elements (FILE_INPUT / MULTI_FILE_INPUT), which are privacyDB-only — flowDB has +// no file support. This is privacyDB's "ElementType extends BaseElementType + file" +// (enums can't be extended in TS, so the base and file enums are merged here). +export const ElementType = { ...BaseElementType, ...FileElementType }; +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type ElementType = BaseElementType | FileElementType; + +// privacyDB collect options. Extends the @core marker and owns the full shape, +// including `tokens` (privacyDB honours it; flowDB has none). +export interface ICollectOptions extends ICollectOptionsBase { + tokens?: boolean, + additionalFields?: IInsertRecordInput, + upsert?: Array, +} + +// privacyDB collect element input: shared base + privacyDB identity keys +// (`table`/`skyflowID`). Shadows the identity-neutral @core `CollectElementInput` +// re-exported by `export * from '@core/types'` above (a local named export wins +// over a star re-export), so consumers see the privacyDB naming. +export interface CollectElementInput extends ICollectElementInputBase { + type: ElementType, + table?: string, + skyflowID?: string, +} + +// privacyDB collect-element update options: shared base + privacyDB identity keys. +// Binds `CollectElement`/`CollectContainer`'s `TUpdateOptions` so `element.update()` +// is typed to privacyDB naming (`table`/`skyflowID`). See Decision 2.1. +export interface CollectElementUpdateOptions extends ICollectElementUpdateOptionsBase { + table?: string, + skyflowID?: string, +} + +// privacyDB collect element options: shared base + the privacyDB-only file options +// (flowDB has no file API). See Decision 2.3. +export interface CollectElementOptions extends ICollectElementOptionsBase { + preserveFileName?: boolean, + allowedFileType?: string[], + blockEmptyFiles?: boolean, + maxFileSize?: number, + maxFileCount?: number, +} + +// privacyDB element state: shared base + `value` including `Blob` (file elements). +// See Decision 2.5. +export interface ElementState extends IElementStateBase { + value: string | Object | Blob | undefined, +} diff --git a/packages/skyflow-js/src/utils/helpers/index.ts b/packages/skyflow-js/src/utils/helpers/index.ts new file mode 100644 index 000000000..021f93ddd --- /dev/null +++ b/packages/skyflow-js/src/utils/helpers/index.ts @@ -0,0 +1,109 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import uuid from '@core/libs/uuid'; +import * as coreHelpers from '@core/helpers'; +import * as metricsHelper from '@core/utils/metrics-helper'; +import { SdkInfo } from '@core/client'; + +// SDK telemetry identity, injected at build time (webpack DefinePlugin) / tests +// (jest setupFiles) from this package's own package.json. Replaces the former +// `import SDKDetails from '../../../package.json'`, which under the shared-core +// layout would resolve to the workspace root, not the package. +export const SDK_DETAILS = { name: SDK_NAME, version: SDK_VERSION }; + +export const flattenObject = (obj, roots = [] as any, sep = '.') => Object.keys(obj).reduce((memo, prop: any) => ({ ...memo, ...(Object.prototype.toString.call(obj[prop]) === '[object Object]' ? flattenObject(obj[prop], roots.concat([prop])) : { [roots.concat([prop]).join(sep)]: obj[prop] }) }), {}); + +// Re-bound from @core/helpers (definitions moved there so the shared +// @core/internal/iframe-form + collect element layer can reach them). Bound as +// local consts — not `export … from` — so jest.spyOn(helpers, …) still hooks +// them. +export const formatFrameNameToId = coreHelpers.formatFrameNameToId; + +export const removeSpaces = coreHelpers.removeSpaces; + +// Re-bound from @core/helpers (definition moved there so both SDKs share one +// copy). Bound as a local const — not `export … from` — so jest.spyOn(helpers, +// 'formatVaultURL') still hooks it. +export const formatVaultURL = coreHelpers.formatVaultURL; + +export function checkIfDuplicateExists(arr) { + return new Set(arr).size !== arr.length; +} + +// Re-bound from @core/helpers (definition moved there so @core/validators and +// core's internal frame layer can reach it). Bound as a local const — not +// `export … from` — so jest.spyOn(helpers, 'appendZeroToOne') still hooks it. +export const appendZeroToOne = coreHelpers.appendZeroToOne; + +export const appendMonthFourDigitYears = coreHelpers.appendMonthFourDigitYears; + +export const appendMonthTwoDigitYears = coreHelpers.appendMonthTwoDigitYears; + +export const getReturnValue = coreHelpers.getReturnValue; + +export const domReady = coreHelpers.domReady; + +export const getMaskedOutput = coreHelpers.getMaskedOutput; + +export const copyToClipboard = coreHelpers.copyToClipboard; + +export const handleCopyIconClick = coreHelpers.handleCopyIconClick; + +export const fileValidation = coreHelpers.fileValidation; + +export const vaildateFileName = coreHelpers.vaildateFileName; + +export const styleToString = coreHelpers.styleToString; + +export const getContainerType = coreHelpers.getContainerType; + +export const addSeperatorToCardNumberMask = coreHelpers.addSeperatorToCardNumberMask; + +// Re-bound from @core/helpers (definitions moved there so the shared @core +// reveal-frame base can reach them). Bound as local consts — not `export … from` +// — so jest.spyOn(helpers, …) still hooks them. +export const constructMaskTranslation = coreHelpers.constructMaskTranslation; + +export const formatRevealElementOptions = coreHelpers.formatRevealElementOptions; + +// SDK telemetry / device-identity helpers moved to @core/helpers (variant-neutral; +// only input is this bundle's build-injected SDK identity). Re-bound as consts — +// not `export … from` — so jest.spyOn(helpers, …) still hooks them, and existing +// `../helpers` importers/tests resolve them from this single package surface. +export const getSdkVersionName = metricsHelper.getSdkVersionName; + +export function getSDKNameAndVersion(metaData?: string): SdkInfo { + const nameAndVersion: SdkInfo = { + sdkName: SDK_NAME, + sdkVersion: SDK_VERSION, + }; + if (metaData && metaData !== '' && metaData.split('@').length > 1) { + nameAndVersion.sdkName = metaData.split('@')[0]; + nameAndVersion.sdkVersion = metaData.split('@')[1]; + } + return nameAndVersion; +} + +export const getOSDetails = metricsHelper.getOSDetails; + +export const getBrowserInfo = metricsHelper.getBrowserInfo; + +export const getDeviceType = metricsHelper.getDeviceType; + +export const getMetaObject = metricsHelper.getMetaObject; + +// Re-bound from @core/helpers (definition moved there so the shared +// @core/external/base-skyflow init path can reach it, and so both SDKs share one +// copy). Bound as a local const — not `export … from` — so jest.spyOn(helpers, +// 'checkAndSetForCustomUrl') still hooks it. +export const checkAndSetForCustomUrl = coreHelpers.checkAndSetForCustomUrl; + +export const generateUploadFileName = (fileName:string) => { + const fileExtentsion = fileName?.split('.')?.pop() || ''; + return `${uuid()}${fileExtentsion && `.${fileExtentsion}`}`; +}; + +export const getValueFromName = coreHelpers.getValueFromName; + +export const getAtobValue = coreHelpers.getAtobValue; diff --git a/packages/skyflow-js/src/utils/logs-helper/index.ts b/packages/skyflow-js/src/utils/logs-helper/index.ts new file mode 100644 index 000000000..9a2783b16 --- /dev/null +++ b/packages/skyflow-js/src/utils/logs-helper/index.ts @@ -0,0 +1,10 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// The variant-neutral logging helpers, including `printLog`, live in +// @core/utils/logs-helper (`printLog` reads this bundle's injected +// SDK_NAME/SDK_VERSION). Re-exported here so existing `../logs-helper` +// importers keep resolving them from a single package surface. +export { + LogLevelOptions, EnvOptions, parameterizedString, getElementName, printLog, +} from '@core/utils/logs-helper'; diff --git a/packages/skyflow-js/src/utils/validators/index.ts b/packages/skyflow-js/src/utils/validators/index.ts new file mode 100644 index 000000000..e4b3f0c99 --- /dev/null +++ b/packages/skyflow-js/src/utils/validators/index.ts @@ -0,0 +1,139 @@ +/* eslint-disable max-len */ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import * as coreValidators from '@core/validators'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; +import SkyflowError from '@core/errors'; +import { IRevealElementInput } from '../../external/reveal/reveal-container'; +import { + RedactionType, + MessageType, + CollectElementInput, + LogLevel, +} from '../common'; +import { printLog } from '../logs-helper'; + +// Re-export the variant-neutral validators now living in @core so the existing +// `../utils/validators` importers keep resolving the full set. Bound as local +// consts (not `export *`) so jest.spyOn(validators, ...) hooks still work — +// star/named re-exports compile to non-configurable getters that can't be spied. +export const validateCreditCardNumber = coreValidators.validateCreditCardNumber; +export const detectCardType = coreValidators.detectCardType; +export const validateExpiryYear = coreValidators.validateExpiryYear; +export const validateExpiryMonth = coreValidators.validateExpiryMonth; +export const isValidExpiryDateFormat = coreValidators.isValidExpiryDateFormat; +export const isValidExpiryYearFormat = coreValidators.isValidExpiryYearFormat; +export const isValidURL = coreValidators.isValidURL; +export const isValidRegExp = coreValidators.isValidRegExp; +export const validateCardNumberLengthCheck = coreValidators.validateCardNumberLengthCheck; +export const validateBooleanOptions = coreValidators.validateBooleanOptions; +export const validateExpiryDate = coreValidators.validateExpiryDate; +export const validateInsertRecords = coreValidators.validateInsertRecords; +export const validateUpdateRecord = coreValidators.validateUpdateRecord; +export const validateAdditionalFieldsInCollect = coreValidators.validateAdditionalFieldsInCollect; +export const validateDetokenizeInput = coreValidators.validateDetokenizeInput; +export const validateGetInput = coreValidators.validateGetInput; +export const validateGetByIdInput = coreValidators.validateGetByIdInput; +export const validateDeleteRecords = coreValidators.validateDeleteRecords; +export const validateInitConfig = coreValidators.validateInitConfig; +export const validateUpsertOptions = coreValidators.validateUpsertOptions; +export const validateComposableContainerOptions = coreValidators.validateComposableContainerOptions; +export const validateInputFormatOptions = coreValidators.validateInputFormatOptions; + +// Variant-specific rules kept local: +// - reveal-input validators: the privacyDB reveal input carries skyflowID / +// column / table / redaction-per-record / file-render keys (flowDB's is +// token-only), so these diverge from the flowvault package. +// - collect-input validator: emits a package-specific deprecation warning via +// the Tier-1 per-package logs-helper (printLog), so it stays here. +export const validateRevealElementRecords = (records: IRevealElementInput[]) => { + if (records.length === 0) throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_RECORDS_REVEAL, []); + records.forEach((record: any) => { + if (!(record && Object.prototype.hasOwnProperty.call(record, 'skyflowID'))) { + if (!(record && Object.prototype.hasOwnProperty.call(record, 'token'))) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_TOKEN_KEY_REVEAL, []); + } + if (!record.token) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_TOKEN_ID_REVEAL, []); + } + if (!(typeof record.token === 'string' || record.token instanceof String)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKEN_ID_REVEAL, []); + } + } + + const recordRedaction = record.redaction; + if (recordRedaction) { + if (!Object.values(RedactionType).includes(recordRedaction)) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REDACTION_TYPE_REVEAL, []); + } + } + + if (Object.prototype.hasOwnProperty.call(record, 'label') && typeof record.label !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_LABEL_REVEAL, []); + } + + if (Object.prototype.hasOwnProperty.call(record, 'altText') && typeof record.altText !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ALT_TEXT_REVEAL, []); + } + + if (Object.prototype.hasOwnProperty.call(record, 'format') && typeof record.format !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FORMAT_REVEAL, []); + } + if (Object.prototype.hasOwnProperty.call(record, 'format') && record.format === '') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_FORMAT_REVEAL, []); + } + }); +}; + +export const validateRenderElementRecord = (record: IRevealElementInput) => { + if (!(record && Object.prototype.hasOwnProperty.call(record, 'skyflowID'))) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_SKYFLOWID_KEY_REVEAL, []); + } + if (!record.skyflowID) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_REVEAL, []); + } + if (Object.prototype.hasOwnProperty.call(record, 'skyflowID') && typeof record.skyflowID !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOW_ID_REVEAL, []); + } + if (Object.prototype.hasOwnProperty.call(record, 'skyflowID') && (Object.prototype.hasOwnProperty.call(record, 'token'))) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.SKYFLOW_IDS_AND_TOKEN_BOTH_SPECIFIED, []); + } + if (!(record && Object.prototype.hasOwnProperty.call(record, 'column'))) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_COLUMN_KEY_REVEAL, []); + } + if (!record.column) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_COLUMN_NAME_REVEAL, []); + } + if (Object.prototype.hasOwnProperty.call(record, 'column') && typeof record.column !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_COLUMN_NAME_REVEAL, []); + } + if (!(record && Object.prototype.hasOwnProperty.call(record, 'table'))) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_TABLE_KEY_REVEAL, []); + } + if (!record.table) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_TABLE_REVEAL, []); + } + if (Object.prototype.hasOwnProperty.call(record, 'table') && typeof record.table !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TABLE_REVEAL, []); + } + if (Object.prototype.hasOwnProperty.call(record, 'altText') && typeof record.altText !== 'string') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ALT_TEXT_RENDER, []); + } +}; + +export const validateCollectElementInput = (input: CollectElementInput, logLevel: LogLevel) => { + if (!Object.prototype.hasOwnProperty.call(input, 'type')) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_ELEMENT_TYPE, [], true); + } + if (!input.type) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_TYPE, [], true); + } + if (Object.prototype.hasOwnProperty.call(input, 'altText')) { + printLog(logs.warnLogs.COLLECT_ALT_TEXT_DEPERECATED, MessageType.WARN, logLevel); + } + if (Object.prototype.hasOwnProperty.call(input, 'skyflowID') && !(typeof input.skyflowID === 'string')) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOWID_IN_COLLECT, [], true); + } +}; diff --git a/tests/__mocks__/file-mock.js b/packages/skyflow-js/tests/__mocks__/file-mock.js similarity index 100% rename from tests/__mocks__/file-mock.js rename to packages/skyflow-js/tests/__mocks__/file-mock.js diff --git a/tests/core-utils/collect.test.js b/packages/skyflow-js/tests/api-utils/collect.test.js similarity index 96% rename from tests/core-utils/collect.test.js rename to packages/skyflow-js/tests/api-utils/collect.test.js index 3210103b5..d3fce0d59 100644 --- a/tests/core-utils/collect.test.js +++ b/packages/skyflow-js/tests/api-utils/collect.test.js @@ -1,11 +1,13 @@ -import Client from "../../src/client"; -import { getUpsertColumn, updateRecordsBySkyflowID, constructElementsInsertReq, insertDataInMultipleFiles, insertDataInCollect, updateRecordsBySkyflowIDComposable, checkForElementMatchRule, checkForValueMatch, constructUpdateRecordRequest, constructUpdateRecordResponse } from "../../src/core-utils/collect"; +import Client from "@core/client"; +import { checkForElementMatchRule, checkForValueMatch } from "@core/helpers"; +import { constructElementsInsertReq } from "@core/api-utils/collect"; +import { getUpsertColumn, updateRecordsBySkyflowID, insertDataInMultipleFiles, insertDataInCollect, updateRecordsBySkyflowIDComposable, constructUpdateRecordRequest, constructUpdateRecordResponse } from "../../src/api-utils/collect"; import { ValidationRuleType } from "../../src/utils/common"; -import SKYFLOW_ERROR_CODE from "../../src/utils/constants"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { parameterizedString } from "../../src/utils/logs-helper"; -import { getAccessToken } from "../../src/utils/bus-events"; +import { getAccessToken } from "@core/utils/bus-events"; -jest.mock('../../src/utils/bus-events', () => ({ +jest.mock('@core/utils/bus-events', () => ({ getAccessToken: jest.fn().mockResolvedValue('auth-token'), })); describe("getUpsertColumn fn test", () => { diff --git a/tests/core-utils/collect.test.ts b/packages/skyflow-js/tests/api-utils/collect.test.ts similarity index 85% rename from tests/core-utils/collect.test.ts rename to packages/skyflow-js/tests/api-utils/collect.test.ts index c0a3514f1..f504b0264 100644 --- a/tests/core-utils/collect.test.ts +++ b/packages/skyflow-js/tests/api-utils/collect.test.ts @@ -1,11 +1,11 @@ +import { checkForValueMatch } from "@core/helpers"; +import { constructElementsInsertReq } from "@core/api-utils/collect"; import { getUpsertColumn, - constructElementsInsertReq, - checkForValueMatch, constructUpdateRecordResponse, constructUpdateRecordRequest, -} from "../../src/core-utils/collect"; -import IFrameFormElement from "../../src/core/internal/iframe-form"; +} from "../../src/api-utils/collect"; +import IFrameFormElement from "@core/internal/iframe-form"; import { ICollectOptions, IUpdateOptions, @@ -14,7 +14,7 @@ import { IValidationRule, ValidationRuleType, } from "../../src/utils/common"; -import SKYFLOW_ERROR_CODE from "../../src/utils/constants"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { parameterizedString } from "../../src/utils/logs-helper"; describe("Testing getUpsertColumn method", () => { @@ -132,6 +132,24 @@ describe("Testing constructElementsInsertReq method", () => { ); } }); + + test("does not pollute Object.prototype when merging an additionalFields record into an existing table", () => { + // Collected element data (the merge source) carries a malicious __proto__ + // key as an own property, as it would after JSON parsing. + const insertReq: any = { table1: JSON.parse('{"cvv":"122","__proto__":{"polluted":"yes"}}') }; + const pollutionOptions: ICollectOptions = { + tokens: true, + additionalFields: { + records: [{ table: "table1", fields: { name: "name" } }], + }, + }; + + constructElementsInsertReq(insertReq, {}, pollutionOptions); + + expect(({} as any).polluted).toBeUndefined(); + expect((Object.prototype as any).polluted).toBeUndefined(); + delete (Object.prototype as any).polluted; + }); }); class MockIFrameFormElement { diff --git a/tests/core-utils/delete.test.js b/packages/skyflow-js/tests/api-utils/delete.test.js similarity index 100% rename from tests/core-utils/delete.test.js rename to packages/skyflow-js/tests/api-utils/delete.test.js diff --git a/tests/core-utils/reveal.test.js b/packages/skyflow-js/tests/api-utils/reveal.test.js similarity index 98% rename from tests/core-utils/reveal.test.js rename to packages/skyflow-js/tests/api-utils/reveal.test.js index 898615fd3..93e4c624d 100644 --- a/tests/core-utils/reveal.test.js +++ b/packages/skyflow-js/tests/api-utils/reveal.test.js @@ -2,10 +2,11 @@ Copyright (c) 2022 Skyflow, Inc. */ import Skyflow from '../../src/skyflow'; -import {formatRecordsForClient, formatRecordsForClientComposable, formatRecordsForIframe, formatRecordsForRender, formatForRenderClient, getFileURLFromVaultBySkyflowID, getFileURLForRender, getFileURLFromVaultBySkyflowIDComposable, fetchRecordsByTokenIdComposable} from "../../src/core-utils/reveal"; +import { formatRecordsForIframe } from "@core/api-utils/reveal"; +import {formatRecordsForClient, formatRecordsForClientComposable, formatRecordsForRender, formatForRenderClient, getFileURLFromVaultBySkyflowID, getFileURLForRender, getFileURLFromVaultBySkyflowIDComposable, fetchRecordsByTokenIdComposable} from "../../src/api-utils/reveal"; import { Env, LogLevel } from '../../src/utils/common'; -import { getAccessToken } from '../../src/utils/bus-events'; -import Client from '../../src/client'; +import { getAccessToken } from '@core/utils/bus-events'; +import Client from '@core/client'; import { url } from 'inspector'; const testTokenId = '1677f7bd-c087-4645-b7da-80a6fd1a81a4'; @@ -59,7 +60,7 @@ const skyflow = Skyflow.init({ }); jest.setTimeout(15000); -jest.mock('../../src/utils/bus-events', () => ({ +jest.mock('@core/utils/bus-events', () => ({ getAccessToken: jest.fn( () => Promise.resolve('mockAccessToken') ), diff --git a/tests/client.test.js b/packages/skyflow-js/tests/client.test.js similarity index 99% rename from tests/client.test.js rename to packages/skyflow-js/tests/client.test.js index 48cdc096c..ceb59dc86 100644 --- a/tests/client.test.js +++ b/packages/skyflow-js/tests/client.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import Client from "../src/client"; +import Client from "@core/client"; describe("Client Class",()=>{ beforeEach(()=>{ jest.clearAllMocks(); diff --git a/tests/client.test.ts b/packages/skyflow-js/tests/client.test.ts similarity index 99% rename from tests/client.test.ts rename to packages/skyflow-js/tests/client.test.ts index 5a62d8e62..9e14c1838 100644 --- a/tests/client.test.ts +++ b/packages/skyflow-js/tests/client.test.ts @@ -2,12 +2,12 @@ Copyright (c) 2025 Skyflow, Inc. */ import assert from "assert"; -import Client, { IClientRequest } from "../src/client"; -import SKYFLOW_ERROR_CODE from "../src/utils/constants"; -import logs from "../src/utils/logs"; -import { ClientMetadata } from "../src/core/internal/internal-types"; +import Client, { IClientRequest } from "@core/client"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; +import logs from "@core/utils/logs"; +import { ClientMetadata } from "../src/internal/internal-types"; import { ISkyflow } from "../src/skyflow"; -import SkyflowError from "../src/libs/skyflow-error"; +import SkyflowError from "@core/errors"; const skyflowConfig: ISkyflow = { vaultID: "e20afc3ae1b54f0199f24130e51e0c11", diff --git a/tests/core/external/collect/collect-container.test.js b/packages/skyflow-js/tests/core/external/collect/collect-container.test.js similarity index 85% rename from tests/core/external/collect/collect-container.test.js rename to packages/skyflow-js/tests/core/external/collect/collect-container.test.js index b2e31fe62..b1fbced4a 100644 --- a/tests/core/external/collect/collect-container.test.js +++ b/packages/skyflow-js/tests/core/external/collect/collect-container.test.js @@ -3,20 +3,20 @@ Copyright (c) 2022 Skyflow, Inc. */ import { COLLECT_FRAME_CONTROLLER, - ElementType, + BaseElementType, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_EVENTS_TO_CONTAINER, SKYFLOW_FRAME_CONTROLLER_READY, ELEMENTS, ELEMENT_EVENTS_TO_CLIENT -} from '../../../../src/core/constants'; -import CollectContainer from '../../../../src/core/external/collect/collect-container'; -import * as iframerUtils from '../../../../src/iframe-libs/iframer'; -import SkyflowError from '../../../../src/libs/skyflow-error'; +} from '@core/constants'; +import CollectContainer from '../../../../src/external/collect/collect-container'; +import * as iframerUtils from '@core/iframe-libs/iframer'; +import SkyflowError from '@core/errors'; import Skyflow from '../../../../src/skyflow'; import { LogLevel, Env, ValidationRuleType, ErrorType } from '../../../../src/utils/common'; -import SKYFLOW_ERROR_CODE from '../../../../src/utils/constants'; -import logs from '../../../../src/utils/logs'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import logs from '@core/utils/logs'; import { parameterizedString } from '../../../../src/utils/logs-helper'; global.ResizeObserver = jest.fn(() => ({ @@ -31,7 +31,7 @@ iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); const mockUuid = '1234'; -jest.mock('../../../../src/libs/uuid', () => ({ +jest.mock('@core/libs/uuid', () => ({ __esModule: true, default: jest.fn(() => (mockUuid)), })); @@ -223,7 +223,7 @@ describe('Collect container', () => { document.body.innerHTML = ''; }); it('should throw error when collect call made with no elements ', () => { - const collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.collect().then().catch(err => { expect(err).toBeDefined(); @@ -231,7 +231,7 @@ describe('Collect container', () => { }) }); it('should throw error when collect call made with no elements case2 ', () => { - const collectContainer = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const collectContainer = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.collect().then().catch(err => { expect(err).toBeDefined(); @@ -239,7 +239,7 @@ describe('Collect container', () => { }) }); it('should throw error when uploadfiles call made with no elements ', () => { - const collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.uploadFiles().then().catch(err => { expect(err).toBeDefined(); @@ -247,7 +247,7 @@ describe('Collect container', () => { }) }); it('should throw error when uploadfiles call made with no elements ', () => { - const collectContainer = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const collectContainer = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.uploadFiles().then().catch(err => { expect(err).toBeDefined(); @@ -256,7 +256,7 @@ describe('Collect container', () => { }); it("container collect success", () => { - let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + let collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -300,7 +300,7 @@ describe('Collect container', () => { }); // it.only("container collect error case when set error is called", () => { - // let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + // let collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); // const div1 = document.createElement('div'); // const div2 = document.createElement('div'); @@ -357,7 +357,7 @@ describe('Collect container', () => { it("container collect case when tokens are invalid", () => { - let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + let collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -384,7 +384,7 @@ describe('Collect container', () => { }); it("container collect case when additional fields are invalid", () => { - let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + let collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -411,7 +411,7 @@ describe('Collect container', () => { }); it("container collect case when upsert are invalid", () => { - let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + let collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -437,7 +437,7 @@ describe('Collect container', () => { }) }); it("container collect case when elements are invalid", () => { - let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + let collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -502,7 +502,7 @@ describe('Collect container', () => { }); it('should resolve successfully when collect is called and isSkyflowFrameReady is false', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -570,7 +570,7 @@ describe('Collect container', () => { }); }); it('should throw error when collect is called and isSkyflowFrameReady is false', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -639,7 +639,7 @@ describe('Collect container', () => { }); }); it('should throw error when collect is called and isSkyflowFrameReady is false and tokens is invalid', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -688,7 +688,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and upsert is invalid', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -737,7 +737,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and additionalFields is invalid', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -786,7 +786,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and additionalFields is invalid', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -838,7 +838,7 @@ describe('Collect container', () => { }); it('element type radio or checkox created', async () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -887,7 +887,7 @@ describe('Collect container', () => { }); it('should successfully upload files when elements are mounted', async () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -930,7 +930,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are not created', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }); const uploadPromise = container.uploadFiles(); @@ -941,7 +941,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are not created and skyflow frame controller not ready', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }); const uploadPromise = container.uploadFiles(); @@ -952,7 +952,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are created but not mounted', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -969,7 +969,7 @@ describe('Collect container', () => { }); it('should successfully upload files when elements are mounted', async () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -993,14 +993,14 @@ describe('Collect container', () => { }); it('should throw an error if elements are not mounted', async () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); await expect(container.uploadFiles()).rejects.toThrow(SkyflowError); }); it('should throw an error if elements are not mounted and skyflow frame not ready', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1012,7 +1012,7 @@ describe('Collect container', () => { expect(response).rejects.toThrow(SkyflowError); }); it('should throw an error if elements are not mounted when skyflow frame controller is not ready', () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); Object.defineProperty(container, '#isSkyflowFrameReady', { @@ -1032,7 +1032,7 @@ describe('Collect container', () => { }); it('should handle errors during file upload', async () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -1055,7 +1055,7 @@ describe('Collect container', () => { }); it('should not emit events when isSkyflowFrameReady is false', async () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1077,7 +1077,7 @@ describe('Collect container', () => { }); it('should resolve successfully when file upload is successful', async () => { - const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1129,7 +1129,7 @@ describe('Collect container', () => { }); it('Invalid element type', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, type: 'abc' }); } catch (err) { @@ -1138,7 +1138,7 @@ describe('Collect container', () => { }); it('Invalid table', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1150,7 +1150,7 @@ describe('Collect container', () => { }); it('Invalid column', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1162,7 +1162,7 @@ describe('Collect container', () => { }); it('Invalid validation params, missing element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1178,7 +1178,7 @@ describe('Collect container', () => { }); it('Invalid validation params, invalid collect element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1195,7 +1195,7 @@ describe('Collect container', () => { } }); it('Invalid validation params, invalid collect element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1212,7 +1212,7 @@ describe('Collect container', () => { } }); it('valid validation params, regex match rule', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1232,7 +1232,7 @@ describe('Collect container', () => { it('create valid Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let cvv; try { cvv = container.create(cvvElement); @@ -1244,7 +1244,7 @@ describe('Collect container', () => { }); it('test default options for card_number', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let card_number; try { card_number = container.create(cardNumberElement); @@ -1257,7 +1257,7 @@ describe('Collect container', () => { it('test invalid option for EXPIRATION_DATE', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { format: 'invalid' }); @@ -1269,7 +1269,7 @@ describe('Collect container', () => { it('test valid option for EXPIRATION_DATE', () => { const validFormat = 'YYYY/MM' - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { format: validFormat }); @@ -1280,7 +1280,7 @@ describe('Collect container', () => { }); it('test enableCardIcon option is enabled for elements', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCardIcon: true }); @@ -1292,7 +1292,7 @@ describe('Collect container', () => { }); it('test enableCopy option is enabled for elements', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCopy: true }); @@ -1304,7 +1304,7 @@ describe('Collect container', () => { }); it('test enableCardIcon option is disabled for elements', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCardIcon: false }); @@ -1315,7 +1315,7 @@ describe('Collect container', () => { }); it('test enableCopy option is disabled for elements', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCopy: false }); @@ -1328,7 +1328,7 @@ describe('Collect container', () => { it('test invalid option for EXPIRATION_YEAR', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationYearElement, { format: 'invalid' }); @@ -1340,7 +1340,7 @@ describe('Collect container', () => { it('test valid option for EXPIRATION_YEAR', () => { const validFormat = 'YYYY' - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationYearElement, { format: validFormat }); @@ -1351,13 +1351,13 @@ describe('Collect container', () => { }); it("container collect", () => { - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); container.collect().then().catch(err => { expect(err).toBeDefined(); }) }); it("container create options", () => { - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryDate = container.create({ table: 'pii_fields', column: 'primary_card.cvv', @@ -1374,7 +1374,7 @@ describe('Collect container', () => { }); }); it("container create options 2", () => { - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryDate = container.create({ table: 'pii_fields', column: 'primary_card.cvv', @@ -1392,7 +1392,7 @@ describe('Collect container', () => { }); it('create valid file Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); let file; try { file = container.create(FileElement); @@ -1404,7 +1404,7 @@ describe('Collect container', () => { }); it('skyflowID undefined for file Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1416,7 +1416,7 @@ describe('Collect container', () => { } }); it('empty table for Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1428,7 +1428,7 @@ describe('Collect container', () => { } }); it('invalid table for Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1441,7 +1441,7 @@ describe('Collect container', () => { } }); it('invalid table for Element case 2', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1454,7 +1454,7 @@ describe('Collect container', () => { } }); it('missing column for Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1466,7 +1466,7 @@ describe('Collect container', () => { } }); it('invalid column for Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1479,7 +1479,7 @@ describe('Collect container', () => { } }); it('invalid column for Element case 2', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1492,7 +1492,7 @@ describe('Collect container', () => { } }); it('invalid column for Element case 2', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1504,7 +1504,7 @@ describe('Collect container', () => { } }); it('skyflowID is missing for file Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1514,7 +1514,7 @@ describe('Collect container', () => { } }); it('skyflowID empty for file Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1526,7 +1526,7 @@ describe('Collect container', () => { } }); it('skyflowID null for file Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1538,7 +1538,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for file Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1550,7 +1550,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for file Element another case', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1562,7 +1562,7 @@ describe('Collect container', () => { } }); it('skyflowID undefined for collect Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1574,7 +1574,7 @@ describe('Collect container', () => { } }); it('skyflowID empty for collect Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1586,7 +1586,7 @@ describe('Collect container', () => { } }); it('skyflowID null for collect Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1598,7 +1598,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for collect Element', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1610,7 +1610,7 @@ describe('Collect container', () => { } }); it('skyflowID null for collect Element another case', () => { - const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1623,7 +1623,7 @@ describe('Collect container', () => { }); it("container collect options", () => { - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const options = { tokens: true, additionalFields: { @@ -1652,7 +1652,7 @@ describe('Collect container', () => { }) }); it("container collect options error case 2", () => { - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const element1 = container.create(cvvElement2); const options = { tokens: true, @@ -1680,7 +1680,7 @@ describe('Collect container', () => { const div1 = document.createElement('div'); const div2 = document.createElement('div'); - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); element1.mount(div1); @@ -1701,7 +1701,7 @@ describe('Collect container', () => { }); it("container collect options error", () => { - let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }); const options = { tokens: true, additionalFields: { @@ -1758,7 +1758,7 @@ describe('iframe cleanup logic', () => { it('should remove unmounted iframe elements', () => { // Create and mount elements - container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -1789,7 +1789,7 @@ describe('iframe cleanup logic', () => { }); it('should handle empty document.body', () => { - container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + container = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const element1 = container.create(cvvElement); element1.mount(div1); @@ -1821,7 +1821,7 @@ describe('iframe cleanup logic', () => { }); it('should remove unmounted iframe elements', () => { - container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); + container = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); // Create and mount elements const element1 = container.create(cvvElement); diff --git a/tests/core/external/collect/collect-container.test.ts b/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts similarity index 90% rename from tests/core/external/collect/collect-container.test.ts rename to packages/skyflow-js/tests/core/external/collect/collect-container.test.ts index c8a242951..b0b83306f 100644 --- a/tests/core/external/collect/collect-container.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts @@ -3,13 +3,13 @@ Copyright (c) 2025 Skyflow, Inc. */ import { ELEMENT_EVENTS_TO_IFRAME, - ElementType, -} from "../../../../src/core/constants"; -import CollectContainer from "../../../../src/core/external/collect/collect-container"; -import CollectElement from "../../../../src/core/external/collect/collect-element"; -import SkyflowContainer from "../../../../src/core/external/skyflow-container"; -import { Metadata } from "../../../../src/core/internal/internal-types"; -import * as iframerUtils from "../../../../src/iframe-libs/iframer"; + BaseElementType, FileElementType, +} from "@core/constants"; +import CollectContainer from "../../../../src/external/collect/collect-container"; +import CollectElement from "@core/external/collect/collect-element"; +import SkyflowContainer from "../../../../src/external/skyflow-container"; +import { Metadata } from "../../../../src/internal/internal-types"; +import * as iframerUtils from "@core/iframe-libs/iframer"; import { ContainerType } from "../../../../src/skyflow"; import { LogLevel, @@ -38,7 +38,7 @@ jest const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); const mockUuid = "1234"; -jest.mock("../../../../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => mockUuid), })); @@ -88,27 +88,27 @@ const cvvInput: CollectElementInput = { column: "primary_card.cvv", placeholder: "cvv", label: "cvv", - type: ElementType.CVV, + type: BaseElementType.CVV, ...collectStylesOptions, }; const cardNumberInput: CollectElementInput = { table: "pii_fields", column: "primary_card.card_number", - type: ElementType.CARD_NUMBER, + type: BaseElementType.CARD_NUMBER, ...collectStylesOptions, }; const ExpirationDateInput: CollectElementInput = { table: "pii_fields", column: "primary_card.expiry", - type: ElementType.EXPIRATION_DATE, + type: BaseElementType.EXPIRATION_DATE, }; const fileInput: CollectElementInput = { table: "pii_fields", column: "primary_card.file", - type: ElementType.FILE_INPUT, + type: FileElementType.FILE_INPUT, skyflowID: "abc-def", }; @@ -150,7 +150,7 @@ describe("Collect container", () => { }); it("should successfully collect data from elements", () => { - const collectContainer = new CollectContainer(metaData, [], { + const collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -191,7 +191,7 @@ describe("Collect container", () => { }); it("tests different collect element options for elements", () => { - const collectContainer = new CollectContainer(metaData, [], { + const collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -210,7 +210,7 @@ describe("Collect container", () => { expect(options.enableCopy).toBe(true); }); it("should successfully collect data from elements, call set error", () => { - const collectContainer = new CollectContainer(metaData, [], { + const collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -252,7 +252,7 @@ describe("Collect container", () => { }); }); it("should successfully upload files when elements are mounted", async () => { - const collectContainer = new CollectContainer(metaData, [], { + const collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -319,7 +319,7 @@ describe("iframe cleanup logic", () => { it("should remove unmounted iframe elements", () => { // Create and mount elements - collectContainer = new CollectContainer(metaData, [], { + collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -352,7 +352,7 @@ describe("iframe cleanup logic", () => { }); it("should handle empty document.body", () => { - collectContainer = new CollectContainer(metaData, [], { + collectContainer = new CollectContainer(metaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -387,7 +387,7 @@ describe("iframe cleanup logic", () => { }); it("should remove unmounted iframe elements", () => { - collectContainer = new CollectContainer(metaData2, [], { + collectContainer = new CollectContainer(metaData2, { logLevel: LogLevel.ERROR, env: Env.PROD, }); diff --git a/tests/core/external/collect/collect-element.test.js b/packages/skyflow-js/tests/core/external/collect/collect-element.test.js similarity index 90% rename from tests/core/external/collect/collect-element.test.js rename to packages/skyflow-js/tests/core/external/collect/collect-element.test.js index 7c4c0d9bc..83859fa20 100644 --- a/tests/core/external/collect/collect-element.test.js +++ b/packages/skyflow-js/tests/core/external/collect/collect-element.test.js @@ -2,19 +2,21 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import CollectElement from '../../../../src/core/external/collect/collect-element'; -import SkyflowError from '../../../../src/libs/skyflow-error'; +import CollectElement from '@core/external/collect/collect-element'; +import SkyflowError from '@core/errors'; import { LogLevel, Env, ValidationRuleType } from '../../../../src/utils/common'; -import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '../../../../src/core/constants'; -import SKYFLOW_ERROR_CODE from '../../../../src/utils/constants'; -import { checkForElementMatchRule } from '../../../../src/core-utils/collect'; +import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, BaseElementType, FileElementType } from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import { checkForElementMatchRule } from '@core/helpers'; import { ContainerType } from '../../../../src/skyflow'; -import EventEmitter from '../../../../src/event-emitter'; +import EventEmitter from '@core/event-emitter'; global.ResizeObserver = jest.fn(() => ({ observe: jest.fn(), disconnect: jest.fn(), })); +// Test stub for the container-injected collect key strategy (Decision 2.2). +const MOCK_COLLECT_VARIANT = { normalizeUpdateOptions: () => {}, skyflowIdKey: 'skyflowID' }; const clientDomain = "http://abc.com"; const elementName = 'element:CVV:cGlpX2ZpZWxkcy5wcmltYXJ5X2NhcmQuY3Z2'; const id = 'id'; @@ -118,7 +120,7 @@ const groupEmiitter = { }) } -jest.mock('../../../../src/event-emitter'); +jest.mock('@core/event-emitter'); let emitterSpy; EventEmitter.mockImplementation(() => ({ on: jest.fn().mockImplementation((name, cb) => {emitterSpy = cb}), @@ -153,7 +155,7 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls .filter((data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT+ elementName); @@ -214,7 +216,7 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls[1][0] expect(inputEvent).toBe(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT+ elementName); @@ -264,7 +266,7 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); const inputCb = onSpy.mock.calls[1][1]; const inputEvent = onSpy.mock.calls[1][0] @@ -313,6 +315,7 @@ describe('collect element', () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -365,6 +368,7 @@ describe('collect element', () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -415,7 +419,8 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD },groupEmiitter); + { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT,groupEmiitter); // groupOnCb({containerId:'containerId'}); expect(() => { element.mount('#123'); }).not.toThrow(SkyflowError); @@ -432,7 +437,7 @@ describe('collect element', () => { false, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); } catch (err) { console.log(err); expect(err).toBeDefined(); @@ -451,7 +456,8 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD },groupEmiitter); + { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT,groupEmiitter); const div = document.createElement('div'); @@ -485,7 +491,8 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD },groupEmiitter); + { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT,groupEmiitter); const div = document.createElement('div'); @@ -520,7 +527,8 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD },groupEmiitter); + { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT,groupEmiitter); const div = document.createElement('div'); @@ -555,7 +563,8 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD },groupEmiitter); + { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT,groupEmiitter); const div = document.createElement('div'); @@ -582,7 +591,7 @@ describe('collect element', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); const options = element.getOptions(); expect(options.name).toBe(input.column); @@ -598,7 +607,7 @@ describe('collect element', () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls .filter((data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT+ elementName); @@ -631,7 +640,7 @@ describe('collect element', () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); expect(element.isMounted()).toBe(false); expect(element.isUpdateCalled()).toBe(false); element.update({ label :'Henry' }); @@ -649,7 +658,7 @@ describe('collect element', () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls .filter((data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT+ elementName); @@ -684,7 +693,7 @@ const row = { }; describe('collect element validations', () => { - it('Invalid ElementType', () => { + it('Invalid BaseElementType', () => { const invalidElementType = [ { elements: [ @@ -706,7 +715,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -736,7 +745,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -766,7 +775,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -798,7 +807,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -830,7 +839,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -863,7 +872,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -898,7 +907,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -933,7 +942,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -967,7 +976,7 @@ describe('collect element validations', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); } catch (err) { expect(err).toBeUndefined(); } @@ -987,7 +996,7 @@ describe('collect element methods', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); const testCollectElementDev = new CollectElement(id, { elementName, @@ -998,7 +1007,7 @@ describe('collect element methods', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.DEV }); + { logLevel: LogLevel.ERROR, env: Env.DEV }, MOCK_COLLECT_VARIANT); it('setError method', () => { testCollectElementProd.setError('ErrorText'); @@ -1132,7 +1141,7 @@ describe('collect element methods', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); let div = document.createElement('div') div.setAttribute('id', 'id1') testCollectElementProd.mount(div); @@ -1162,7 +1171,7 @@ describe('collect element methods', () => { true, destroyCallback, updateCallback, - { logLevel: LogLevel.ERROR, env: Env.PROD }); + { logLevel: LogLevel.ERROR, env: Env.PROD }, MOCK_COLLECT_VARIANT); let div = document.createElement('div') div.setAttribute('id', 'id1') document.body.appendChild(div); @@ -1181,12 +1190,13 @@ describe('collect element methods', () => { const composableRowsForTest = [ { elements: [ { composableElementName, elementType: input.type, elementName, name: input.column, labelStyles, errorTextStyles, ...input }, - { composableElementName, elementType: ElementType.MULTI_FILE_INPUT, elementName: multiFileElementName, name: 'files', labelStyles, errorTextStyles, table: 'pii_fields', column: 'primary_card.files' } + { composableElementName, elementType: FileElementType.MULTI_FILE_INPUT, elementName: multiFileElementName, name: 'files', labelStyles, errorTextStyles, table: 'pii_fields', column: 'primary_card.files' } ] } ]; const groupEmitterLocal = { _emit: jest.fn(), on: jest.fn() }; // isSingleElementAPI must be false for composable container to keep state.value an object - const collectEl = new CollectElement(id, { elementName, rows: composableRowsForTest }, { containerType: ContainerType.COMPOSABLE }, { type: ContainerType.COMPOSABLE, containerId: 'containerId', isMounted: true }, false, destroyCallback, updateCallback, { logLevel: LogLevel.INFO, env: Env.DEV }, groupEmitterLocal); + const collectEl = new CollectElement(id, { elementName, rows: composableRowsForTest }, { containerType: ContainerType.COMPOSABLE }, { type: ContainerType.COMPOSABLE, containerId: 'containerId', isMounted: true }, false, destroyCallback, updateCallback, { logLevel: LogLevel.INFO, env: Env.DEV }, + MOCK_COLLECT_VARIANT, groupEmitterLocal); const dispatchEventFor = (targetElementName, eventType, valueObj = {}) => { window.dispatchEvent(new MessageEvent('message', { @@ -1233,12 +1243,13 @@ describe('collect element methods', () => { const composableRowsForTest = [ { elements: [ { composableElementName, elementType: input.type, elementName, name: input.column, labelStyles, errorTextStyles, ...input }, - { composableElementName, elementType: ElementType.MULTI_FILE_INPUT, elementName: multiFileElementName, name: 'files', labelStyles, errorTextStyles, table: 'pii_fields', column: 'primary_card.files' } + { composableElementName, elementType: FileElementType.MULTI_FILE_INPUT, elementName: multiFileElementName, name: 'files', labelStyles, errorTextStyles, table: 'pii_fields', column: 'primary_card.files' } ] } ]; const groupEmitterLocal = { _emit: jest.fn(), on: jest.fn() }; // isSingleElementAPI must be false for composable container to keep state.value an object - const collectEl = new CollectElement(id, { elementName, rows: composableRowsForTest }, { containerType: ContainerType.COMPOSABLE }, { type: ContainerType.COMPOSABLE, containerId: 'containerId', isMounted: true }, true, destroyCallback, updateCallback, { logLevel: LogLevel.INFO, env: Env.DEV }, groupEmitterLocal); + const collectEl = new CollectElement(id, { elementName, rows: composableRowsForTest }, { containerType: ContainerType.COMPOSABLE }, { type: ContainerType.COMPOSABLE, containerId: 'containerId', isMounted: true }, true, destroyCallback, updateCallback, { logLevel: LogLevel.INFO, env: Env.DEV }, + MOCK_COLLECT_VARIANT, groupEmitterLocal); groupEmiitter.on(ELEMENT_EVENTS_TO_CLIENT.READY + ':' + elementName, (data) => { expect(data.name).toBe(elementName); }); diff --git a/tests/core/external/collect/collect-element.test.ts b/packages/skyflow-js/tests/core/external/collect/collect-element.test.ts similarity index 88% rename from tests/core/external/collect/collect-element.test.ts rename to packages/skyflow-js/tests/core/external/collect/collect-element.test.ts index e6a1781a1..efffde3c0 100644 --- a/tests/core/external/collect/collect-element.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/collect-element.test.ts @@ -2,8 +2,8 @@ Copyright (c) 2025 Skyflow, Inc. */ import bus from "framebus"; -import CollectElement from "../../../../src/core/external/collect/collect-element"; -import SkyflowError from "../../../../src/libs/skyflow-error"; +import CollectElement from "@core/external/collect/collect-element"; +import SkyflowError from "@core/errors"; import { LogLevel, Env, @@ -16,13 +16,13 @@ import { import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, - ElementType, -} from "../../../../src/core/constants"; -import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; + BaseElementType, +} from "@core/constants"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { ContainerType } from "../../../../src/skyflow"; -import EventEmitter from "../../../../src/event-emitter"; -import SkyflowContainer from "../../../../src/core/external/skyflow-container"; -import { Metadata } from "../../../../src/core/internal/internal-types"; +import EventEmitter from "@core/event-emitter"; +import SkyflowContainer from "../../../../src/external/skyflow-container"; +import { Metadata } from "../../../../src/internal/internal-types"; global.ResizeObserver = jest.fn(() => ({ observe: jest.fn(), @@ -30,6 +30,8 @@ global.ResizeObserver = jest.fn(() => ({ unobserve: jest.fn(), })); +// Test stub for the container-injected collect key strategy (Decision 2.2). +const MOCK_COLLECT_VARIANT = { normalizeUpdateOptions: () => {}, skyflowIdKey: 'skyflowID' }; const elementName = "element:CVV:cGlpX2ZpZWxkcy5wcmltYXJ5X2NhcmQuY3Z2"; const id = "id"; const input: CollectElementInput = { @@ -42,7 +44,7 @@ const input: CollectElementInput = { }, placeholder: "cvv", label: "cvv", - type: ElementType.CVV, + type: BaseElementType.CVV, }; const composableElementName = @@ -58,7 +60,7 @@ const composableInput: CollectElementInput = { }, placeholder: "XXXX XXXX XXXX XXXX", label: "card number", - type: ElementType.CARD_NUMBER, + type: BaseElementType.CARD_NUMBER, }; const labelStyles: LabelStyles = { @@ -114,7 +116,7 @@ const composableRows = [ ]; const updateElementInput = { - elementType: ElementType.CVV, + elementType: BaseElementType.CVV, name: input.column, ...input, }; @@ -157,7 +159,7 @@ const metaData: Metadata = { } as unknown as SkyflowContainer, }; -jest.mock("../../../../src/event-emitter"); +jest.mock("@core/event-emitter"); let emitterSpy: Function; (EventEmitter as unknown as jest.Mock).mockImplementation(() => ({ @@ -198,7 +200,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls.filter( (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName @@ -261,6 +263,70 @@ describe("testing collect element under various scenarios", () => { }); }); + it("scopes COLLECT_ELEMENT_READY to the element's own iframe (cross-package message isolation)", () => { + // Regression guard for SK-3041 Phase 3 Task 3.6: skyflow-js and + // skyflow-flowvault-js are served from one shared CDN origin, so a + // postMessage origin check cannot distinguish one package's iframe from the + // other's. The COLLECT_ELEMENT_READY round-trip must therefore be scoped to + // this element's unique iframe name, so a sibling (e.g. flowvault) element's + // READY can never resolve this element's READY. + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + , MOCK_COLLECT_VARIANT); + + const iframeName = element.iframeName(); + expect(iframeName.length).toBeGreaterThan(0); + + // The framebus subscription must carry the instance-unique suffix, never the + // bare COLLECT_ELEMENT_READY constant. + const readyOnCalls = onSpy.mock.calls.filter( + (c) => + typeof c[0] === "string" && + c[0].startsWith(ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY) + ); + expect(readyOnCalls.length).toBe(1); + const subscribedEvent = readyOnCalls[0][0]; + expect(subscribedEvent).not.toBe( + ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY + ); + expect(subscribedEvent).toBe( + ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY + iframeName + ); + + // The emit side publishes on the SAME instance-scoped channel. + element.on(ELEMENT_EVENTS_TO_CLIENT.READY, jest.fn()); + const readyEmitCalls = emitSpy.mock.calls.filter( + (c) => + typeof c[0] === "string" && + c[0].startsWith(ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY) + ); + expect(readyEmitCalls.length).toBeGreaterThan(0); + expect(readyEmitCalls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY + iframeName + ); + + // A foreign element's READY is a different framebus event string, so it is + // never routed to this element's subscription. + const foreignChannel = + ELEMENT_EVENTS_TO_IFRAME.COLLECT_ELEMENT_READY + + "element:CARD_NUMBER:" + + btoa("foreign-uuid"); + expect(foreignChannel).not.toBe(subscribedEvent); + }); + it("tests constructor for collect element with element mounted", () => { const onSpy = jest.spyOn(bus, "on"); @@ -280,7 +346,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls[1][0]; expect(inputEvent).toBe(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName); @@ -340,7 +406,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputCb = onSpy.mock.calls[1][1]; const inputEvent = onSpy.mock.calls[1][0]; @@ -400,6 +466,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -469,6 +536,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -531,6 +599,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); expect(() => { @@ -556,6 +625,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -590,6 +660,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -632,6 +703,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -674,6 +746,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD }, + MOCK_COLLECT_VARIANT, groupEmiitter ); @@ -706,7 +779,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls.filter( (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName @@ -749,7 +822,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); expect(element.isMounted()).toBe(false); expect(element.isUpdateCalled()).toBe(false); element.update({ label: "Henry" }); @@ -772,7 +845,7 @@ describe("testing collect element under various scenarios", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const inputEvent = onSpy.mock.calls.filter( (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName @@ -812,7 +885,7 @@ const row = { }; describe("testing collect element validations", () => { - it("Invalid ElementType", () => { + it("Invalid BaseElementType", () => { const invalidElementType = [ { elements: [ @@ -841,7 +914,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -878,7 +951,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -915,7 +988,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -960,7 +1033,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -1005,7 +1078,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -1053,7 +1126,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -1101,7 +1174,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -1149,7 +1222,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); }; expect(createElement).toThrow( @@ -1197,7 +1270,7 @@ describe("testing collect element validations", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); } catch (err) { expect(err).toBeUndefined(); } @@ -1223,7 +1296,7 @@ describe("testing collect element methods", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); const testCollectElementDev = new CollectElement( id, @@ -1241,7 +1314,7 @@ describe("testing collect element methods", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.DEV } - ); + , MOCK_COLLECT_VARIANT); it("tests valid on listener return state in handler for element in DEV env", () => { let handlerState; @@ -1302,7 +1375,7 @@ describe("testing collect element methods", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); let div = document.createElement("div"); div.setAttribute("id", "id1"); testCollectElementProd.mount(div); @@ -1340,7 +1413,7 @@ describe("testing collect element methods", () => { destroyCallback, updateCallback, { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + , MOCK_COLLECT_VARIANT); let div = document.createElement("div"); div.setAttribute("id", "id1"); document.body.appendChild(div); diff --git a/tests/core/external/collect/composable-container.test.js b/packages/skyflow-js/tests/core/external/collect/composable-container.test.js similarity index 87% rename from tests/core/external/collect/composable-container.test.js rename to packages/skyflow-js/tests/core/external/collect/composable-container.test.js index e29375b3b..0f9c23246 100644 --- a/tests/core/external/collect/composable-container.test.js +++ b/packages/skyflow-js/tests/core/external/collect/composable-container.test.js @@ -2,20 +2,20 @@ import { COLLECT_FRAME_CONTROLLER, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_EVENTS_TO_CLIENT, - ElementType -} from '../../../../src/core/constants'; -import * as iframerUtils from '../../../../src/iframe-libs/iframer'; + BaseElementType, FileElementType +} from '@core/constants'; +import * as iframerUtils from '@core/iframe-libs/iframer'; import { LogLevel, Env, ValidationRuleType, ErrorType } from '../../../../src/utils/common'; -import logs from '../../../../src/utils/logs'; -import ComposableContainer from "../../../../src/core/external/collect/compose-collect-container"; -import ComposableElement from '../../../../src/core/external/collect/compose-collect-element'; -import CollectElement from '../../../../src/core/external/collect/collect-element'; -import SKYFLOW_ERROR_CODE from '../../../../src/utils/constants'; -import EventEmitter from '../../../../src/event-emitter'; +import logs from '@core/utils/logs'; +import ComposableContainer from "../../../../src/external/collect/compose-collect-container"; +import ComposableElement from '../../../../src/external/collect/compose-collect-element'; +import CollectElement from '@core/external/collect/collect-element'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; +import EventEmitter from '@core/event-emitter'; import { parameterizedString } from '../../../../src/utils/logs-helper'; -import { SKYFLOW_FRAME_CONTROLLER_READY } from '../../../../src/core/constants'; -import SkyflowError from '../../../../src/libs/skyflow-error'; -import properties from '../../../../src/properties'; +import { SKYFLOW_FRAME_CONTROLLER_READY } from '@core/constants'; +import SkyflowError from '@core/errors'; +import properties from '@core/properties'; const bus = require('framebus'); @@ -24,14 +24,14 @@ iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); const mockUuid = '1234'; -jest.mock('../../../../src/libs/uuid', () => ({ +jest.mock('@core/libs/uuid', () => ({ __esModule: true, default: jest.fn(() => (mockUuid)), })); const mockUnmount = jest.fn(); const updateMock = jest.fn(); -jest.mock('../../../../src/core/external/collect/collect-element'); +jest.mock('@core/external/collect/collect-element'); CollectElement.mockImplementation((_,tempElements)=>{ tempElements.rows[0].elements.forEach((element)=>{ element.isMounted = true; @@ -44,7 +44,7 @@ CollectElement.mockImplementation((_,tempElements)=>{ updateElement:updateMock }}) -jest.mock('../../../../src/event-emitter'); +jest.mock('@core/event-emitter'); const emitMock = jest.fn(); let emitterSpy; let composableUpdateSpy; @@ -147,7 +147,7 @@ const cardNumberElement = { const FileInuptElement = { table: 'pii_fields', column: 'profile_picture', - type: ElementType.FILE_INPUT, + type: FileElementType.FILE_INPUT, skyflowID:'id1', ...collectStylesOptions, } @@ -202,17 +202,17 @@ describe('test composable container class',()=>{ it('test constructor', () => { - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); expect(container).toBeInstanceOf(ComposableContainer); }); it('test create method',()=>{ - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); const element = container.create(cvvElement); expect(element).toBeInstanceOf(ComposableElement); }); it('should throw error when create method is called with no element',(done)=>{ - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); container.collect().catch((err) => { done(); expect(err).toBeDefined(); @@ -224,7 +224,7 @@ describe('test composable container class',()=>{ }) it('should throw error when create method is called with no element case 2',(done)=>{ - const container = new ComposableContainer(metaData2, {}, context, {layout:[1]}); + const container = new ComposableContainer(metaData2, context, {layout:[1]}); container.collect().catch((err) => { done(); expect(err).toBeDefined(); @@ -236,7 +236,7 @@ describe('test composable container class',()=>{ }) it('test create method with callback',()=>{ - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); const element = container.create(cvvElement); // on.mock.calls[0][1]({name : "collect_controller1234"},()=>{}); // on.mock.calls[1][1]({name : "collect_controller"},()=>{}); @@ -247,7 +247,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData, [], context, {layout:[2]}); + const container = new ComposableContainer(metaData, context, {layout:[2]}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -262,7 +262,6 @@ describe('test composable container class',()=>{ const container = new ComposableContainer( metaData, - {}, context, { layout: [2], styles: { base: { width: '100px' } } } ); @@ -330,7 +329,6 @@ describe('test composable container class',()=>{ const container = new ComposableContainer( metaData, - {}, context, { layout: [2], styles: { base: { width: '100px' } } } ); @@ -396,7 +394,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); + const container = new ComposableContainer(metaData, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -420,7 +418,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); + const container = new ComposableContainer(metaData, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -442,7 +440,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); + const container = new ComposableContainer(metaData, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -479,7 +477,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData2, {}, context, {layout:[2],styles:{base:{width:'100px',}}}); + const container = new ComposableContainer(metaData2, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -501,7 +499,7 @@ describe('test composable container class',()=>{ div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData2, {}, context, {layout:[2],styles:{base:{width:'100px',}}}); + const container = new ComposableContainer(metaData2, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -521,7 +519,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); + const container = new ComposableContainer(metaData, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); try{ @@ -539,7 +537,7 @@ describe('test composable container class',()=>{ it("test container collect", () => { const containerOptions = {layout:[2],styles:{base:{width:'100px'}},errorTextStyles:{base:{color:'red'}}}; - let container = new ComposableContainer(metaData, [], context, containerOptions); + let container = new ComposableContainer(metaData, context, containerOptions); // const div = document.createElement('div'); // div.id = 'composable' // document.body.append(div); @@ -581,7 +579,7 @@ describe('test composable container class',()=>{ div.id = 'composable' document.body.append(div); - const container = new ComposableContainer(metaData, [], context, {layout:[2]}); + const container = new ComposableContainer(metaData, context, {layout:[2]}); // const frameReadyCb = on.mock.calls[0][1]; // const cb2 = jest.fn(); // frameReadyCb({ @@ -599,7 +597,7 @@ describe('test composable container class',()=>{ it('test on method without parameters will throw error',()=>{ try{ - const container = new ComposableContainer(metaData, [], context, {layout:[1]},); + const container = new ComposableContainer(metaData, context, {layout:[1]},); const element = container.create(cvvElement); container.on(); expect(element).toBeInstanceOf(ComposableElement); @@ -610,7 +608,7 @@ describe('test composable container class',()=>{ it('test on method without event name will throw error',()=>{ try { - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE"); expect(element).toBeInstanceOf(ComposableElement); @@ -621,7 +619,7 @@ describe('test composable container class',()=>{ it('test on method passing handler as invalid type will throw error',()=>{ try { - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE","test"); expect(element).toBeInstanceOf(ComposableElement); @@ -631,7 +629,7 @@ describe('test composable container class',()=>{ }); it('test on method without error',()=>{ - const container = new ComposableContainer(metaData, [], context, {layout:[1]}); + const container = new ComposableContainer(metaData, context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE",()=>{}); expect(element).toBeInstanceOf(ComposableElement); @@ -643,7 +641,6 @@ describe('test composable container class',()=>{ const container = new ComposableContainer( metaData, - {}, context, { layout: [1], styles: { base: { width: '100px' } } } ); @@ -709,7 +706,7 @@ describe('test composable container class',()=>{ div.id = 'composable2'; document.body.append(div); - const container = new ComposableContainer(metaDataFail, {}, context, { layout: [1] }); + const container = new ComposableContainer(metaDataFail, context, { layout: [1] }); const element1 = container.create(FileInuptElement); container.mount('#composable2'); @@ -727,7 +724,7 @@ describe('test composable container class',()=>{ // Mock getRootNode to return shadowRoot shadowDiv.getRootNode = jest.fn(() => shadowRoot); - const container = new ComposableContainer(metaData, [], context, { layout: [2] }); + const container = new ComposableContainer(metaData, context, { layout: [2] }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -765,7 +762,7 @@ describe('test composable container class',()=>{ // Mock getRootNode to return shadowRoot shadowDiv.getRootNode = jest.fn(() => shadowRoot); - const container = new ComposableContainer(metaData, [], context, { layout: [2] }); + const container = new ComposableContainer(metaData, context, { layout: [2] }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -788,7 +785,7 @@ describe('test composable container class',()=>{ // Mock getRootNode to return document (not a ShadowRoot) div.getRootNode = jest.fn(() => document); - const container = new ComposableContainer(metaData, [], context, { layout: [2] }); + const container = new ComposableContainer(metaData, context, { layout: [2] }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -823,7 +820,7 @@ describe('test composable container class',()=>{ shadowDiv.getRootNode = jest.fn(() => shadowRoot); - const container = new ComposableContainer(metaData, [], context, { layout: [2] }); + const container = new ComposableContainer(metaData, context, { layout: [2] }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); diff --git a/tests/core/external/collect/composable-container.test.ts b/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts similarity index 85% rename from tests/core/external/collect/composable-container.test.ts rename to packages/skyflow-js/tests/core/external/collect/composable-container.test.ts index cd2ba7820..71657c4bd 100644 --- a/tests/core/external/collect/composable-container.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts @@ -3,8 +3,8 @@ */ import { ELEMENT_EVENTS_TO_IFRAME, - ElementType, -} from "../../../../src/core/constants"; + BaseElementType, +} from "@core/constants"; import { LogLevel, Env, @@ -14,18 +14,18 @@ import { Context, ICollectOptions, } from "../../../../src/utils/common"; -import ComposableContainer from "../../../../src/core/external/collect/compose-collect-container"; -import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; -import CollectElement from "../../../../src/core/external/collect/collect-element"; -import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; -import EventEmitter from "../../../../src/event-emitter"; +import ComposableContainer from "../../../../src/external/collect/compose-collect-container"; +import ComposableElement from "../../../../src/external/collect/compose-collect-element"; +import CollectElement from "@core/external/collect/collect-element"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; +import EventEmitter from "@core/event-emitter"; import { parameterizedString } from "../../../../src/utils/logs-helper"; -import SkyflowError from "../../../../src/libs/skyflow-error"; -import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import SkyflowError from "@core/errors"; +import SkyflowContainer from "../../../../src/external/skyflow-container"; import { ContainerType } from "../../../../src/skyflow"; -import { Metadata } from "../../../../src/core/internal/internal-types"; -import IFrame from "../../../../src/core/external/common/iframe"; -import properties from "../../../../src/properties"; +import { Metadata } from "../../../../src/internal/internal-types"; +import IFrame from "@core/external/common/iframe"; +import properties from "@core/properties"; global.ResizeObserver = jest.fn(() => ({ observe: jest.fn(), @@ -35,9 +35,9 @@ global.ResizeObserver = jest.fn(() => ({ const bus = require("framebus"); -jest.mock("../../../../src/iframe-libs/iframer", () => { +jest.mock("@core/iframe-libs/iframer", () => { const actualModule = jest.requireActual( - "../../../../src/iframe-libs/iframer" + "@core/iframe-libs/iframer" ); const mockedModule = { ...actualModule }; mockedModule.__esModule = true; @@ -48,14 +48,14 @@ jest.mock("../../../../src/iframe-libs/iframer", () => { const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve("token")); const mockUuid = "1234"; -jest.mock("../../../../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => mockUuid), })); const mockUnmount = jest.fn(); const updateMock = jest.fn(); -jest.mock("../../../../src/core/external/collect/collect-element"); +jest.mock("@core/external/collect/collect-element"); (CollectElement as unknown as jest.Mock).mockImplementation( (_, tempElements) => { @@ -72,7 +72,7 @@ jest.mock("../../../../src/core/external/collect/collect-element"); } ); -jest.mock("../../../../src/event-emitter"); +jest.mock("@core/event-emitter"); const emitMock = jest.fn(); let emitterSpy: Function; @@ -132,7 +132,7 @@ const cvvElementInput: CollectElementInput = { column: "primary_card.cvv", placeholder: "cvv", label: "cvv", - type: ElementType.CVV, + type: BaseElementType.CVV, validations: [ { type: ValidationRuleType.LENGTH_MATCH_RULE, @@ -149,7 +149,7 @@ const cvvElementInput: CollectElementInput = { const cardNumberElement: CollectElementInput = { table: "pii_fields", column: "primary_card.card_number", - type: ElementType.CARD_NUMBER, + type: BaseElementType.CARD_NUMBER, ...collectStylesOptions, }; @@ -190,14 +190,14 @@ describe("test composable container class", () => { }); it("tests constructor", () => { - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [1], }); expect(container).toBeInstanceOf(ComposableContainer); }); it("tests create method", () => { - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [1], }); const element = container.create(cvvElementInput); @@ -205,7 +205,7 @@ describe("test composable container class", () => { }); it("should throw error when create method is called with no element", (done) => { - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [1], }); container.collect().catch((err) => { @@ -224,7 +224,7 @@ describe("test composable container class", () => { }); it("should throw error when create method is called with no element case 2", (done) => { - const container = new ComposableContainer(metaData2, [], context, { + const container = new ComposableContainer(metaData2, context, { layout: [1], }); container.collect().catch((err) => { @@ -243,7 +243,7 @@ describe("test composable container class", () => { }); it("test create method with callback", () => { - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [1], }); const element = container.create(cvvElementInput); @@ -254,7 +254,7 @@ describe("test composable container class", () => { const div = document.createElement("div"); div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [2], }); const element1 = container.create(cvvElementInput); @@ -267,7 +267,7 @@ describe("test composable container class", () => { const div = document.createElement("div"); div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [2], styles: { base: { width: "100px" } }, }); @@ -330,7 +330,7 @@ describe("test composable container class", () => { const div = document.createElement("div"); div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [2], styles: { base: { width: "100px" } }, }); @@ -360,7 +360,7 @@ describe("test composable container class", () => { div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData2, [], context, { + const container = new ComposableContainer(metaData2, context, { layout: [2], styles: { base: { width: "100px" } }, }); @@ -382,7 +382,7 @@ describe("test composable container class", () => { const div = document.createElement("div"); div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [2], styles: { base: { width: "100px" } }, }); @@ -415,7 +415,6 @@ describe("test composable container class", () => { }; let container = new ComposableContainer( metaData, - [], context, containerOptions ); @@ -456,7 +455,7 @@ describe("test composable container class", () => { div.id = "composable"; document.body.append(div); - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [2], }); const element1 = container.create(cvvElementInput); diff --git a/tests/core/external/collect/composable-element.test.js b/packages/skyflow-js/tests/core/external/collect/composable-element.test.js similarity index 96% rename from tests/core/external/collect/composable-element.test.js rename to packages/skyflow-js/tests/core/external/collect/composable-element.test.js index 4c93f742f..1b25e1ecc 100644 --- a/tests/core/external/collect/composable-element.test.js +++ b/packages/skyflow-js/tests/core/external/collect/composable-element.test.js @@ -1,4 +1,4 @@ -import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; +import ComposableElement from "../../../../src/external/collect/compose-collect-element"; import { ContainerType } from "../../../../src/skyflow"; describe("test composable element", () => { diff --git a/tests/core/external/collect/composable-element.test.ts b/packages/skyflow-js/tests/core/external/collect/composable-element.test.ts similarity index 94% rename from tests/core/external/collect/composable-element.test.ts rename to packages/skyflow-js/tests/core/external/collect/composable-element.test.ts index 342764670..3c5b20d0e 100644 --- a/tests/core/external/collect/composable-element.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/composable-element.test.ts @@ -1,13 +1,13 @@ /* Copyright (c) 2025 Skyflow, Inc. */ -import { ELEMENT_EVENTS_TO_IFRAME, ElementType } from "../../../../src/core/constants"; -import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; -import EventEmitter from "../../../../src/event-emitter"; +import { ELEMENT_EVENTS_TO_IFRAME, BaseElementType, FileElementType } from "@core/constants"; +import ComposableElement from "../../../../src/external/collect/compose-collect-element"; +import EventEmitter from "@core/event-emitter"; import { ContainerType } from "../../../../src/skyflow"; import { ElementState } from "../../../../src/utils/common"; -import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; -import properties from "../../../../src/properties"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; +import properties from "@core/properties"; describe("test composable element", () => { const emitter = jest.fn(); @@ -212,7 +212,7 @@ describe("test composable element", () => { it('uploadMultipleFiles resolves on success message event', async () => { const elementName = 'multiSuccess'; const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; - const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: FileElementType.MULTI_FILE_INPUT }); let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; @@ -228,7 +228,7 @@ describe("test composable element", () => { it('uploadMultipleFiles rejects when message has errorResponse', async () => { const elementName = 'multiErrResp'; const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; - const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: FileElementType.MULTI_FILE_INPUT }); let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; }); const promise = multiEl.uploadMultipleFiles(); @@ -240,7 +240,7 @@ describe("test composable element", () => { it('uploadMultipleFiles rejects when message has error field', async () => { const elementName = 'multiErrField'; const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; - const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: FileElementType.MULTI_FILE_INPUT }); let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; }); const promise = multiEl.uploadMultipleFiles(); @@ -251,7 +251,7 @@ describe("test composable element", () => { it('uploadMultipleFiles ignores message from wrong origin', async () => { const elementName = 'multiWrongOrigin'; const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; - const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: FileElementType.MULTI_FILE_INPUT }); let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; @@ -268,7 +268,7 @@ describe("test composable element", () => { it('uploadMultipleFiles ignores message with wrong event type', async () => { const elementName = 'multiWrongType'; const emitterStub: any = { _emit: jest.fn(), on: jest.fn() }; - const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: ElementType.MULTI_FILE_INPUT }); + const multiEl = new ComposableElement(elementName, emitterStub, iframeName, { type: FileElementType.MULTI_FILE_INPUT }); let messageHandler: any; const addSpy = jest.spyOn(window, 'addEventListener').mockImplementation((evt, handler) => { if (evt === 'message') messageHandler = handler; diff --git a/tests/core/external/common/iframe.test.js b/packages/skyflow-js/tests/core/external/common/iframe.test.js similarity index 94% rename from tests/core/external/common/iframe.test.js rename to packages/skyflow-js/tests/core/external/common/iframe.test.js index c4a84e6f3..42ad1344a 100644 --- a/tests/core/external/common/iframe.test.js +++ b/packages/skyflow-js/tests/core/external/common/iframe.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import IFrame from '../../../../src/core/external/common/iframe'; +import IFrame from '@core/external/common/iframe'; describe('Mount and Unmount Iframe', () => { it('mount and unmount', async () => { diff --git a/tests/core/external/reveal/reveal-composable-container.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js similarity index 91% rename from tests/core/external/reveal/reveal-composable-container.test.js rename to packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js index ea368f53d..48e471ac3 100644 --- a/tests/core/external/reveal/reveal-composable-container.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js @@ -1,19 +1,19 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import RevealContainer from "../../../../src/core/external/reveal/reveal-container"; +import RevealContainer from "../../../../src/external/reveal/reveal-container"; import { ComposableRevealContainer, ComposableRevealElement } from "../../../../src/index-node"; -import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_CONTAINER, ELEMENT_EVENTS_TO_IFRAME, REVEAL_FRAME_CONTROLLER, REVEAL_TYPES } from "../../../../src/core/constants"; +import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_CONTAINER, ELEMENT_EVENTS_TO_IFRAME, REVEAL_FRAME_CONTROLLER, REVEAL_TYPES } from "@core/constants"; import bus from "framebus"; import { LogLevel,Env } from "../../../../src/utils/common"; -import RevealElement from "../../../../src/core/external/reveal/reveal-element"; -import * as iframerUtils from '../../../../src/iframe-libs/iframer'; -import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import RevealElement from "../../../../src/external/reveal/reveal-element"; +import * as iframerUtils from '@core/iframe-libs/iframer'; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { parameterizedString } from "../../../../src/utils/logs-helper"; -import SkyflowError from "../../../../src/libs/skyflow-error"; -import logs from "../../../../src/utils/logs"; +import SkyflowError from "@core/errors"; +import logs from "@core/utils/logs"; // Mock internal element to intercept constructor arguments for mount coverage -jest.mock('../../../../src/core/external/reveal/composable-reveal-internal', () => { +jest.mock('../../../../src/external/reveal/composable-reveal-internal', () => { return { __esModule: true, default: jest.fn().mockImplementation((elementId, recordGroup, metaData, containerProps, context) => { @@ -29,12 +29,12 @@ jest.mock('../../../../src/core/external/reveal/composable-reveal-internal', () }), }; }); -import ComposableRevealInternalElement from '../../../../src/core/external/reveal/composable-reveal-internal'; -import properties from "../../../../src/properties"; +import ComposableRevealInternalElement from '../../../../src/external/reveal/composable-reveal-internal'; +import properties from "@core/properties"; iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); const mockUuid = '1234'; -jest.mock('../../../../src/libs/uuid',()=>({ +jest.mock('@core/libs/uuid',()=>({ __esModule: true, default:jest.fn(()=>(mockUuid)), })); @@ -71,7 +71,7 @@ describe("Reveal Composable Container Class", () => { clientDomain: "http://abc.com", }, }; - const testRevealContainer = new ComposableRevealContainer(testMetaData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(testMetaData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); const skyflowConfig = { @@ -135,7 +135,7 @@ describe("Reveal Composable Container Class", () => { }, }; test("reveal should throw error with no elements", (done) => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); container.reveal().catch((error) => { done(); expect(error).toBeDefined(); @@ -148,7 +148,7 @@ describe("Reveal Composable Container Class", () => { /**************** Mount method lines 246-299 coverage tests ****************/ test('mount() should throw MISMATCH_ELEMENT_COUNT_LAYOUT_SUM when layout sum differs from elements length', () => { const meta = { ...testMetaData }; - const container = new ComposableRevealContainer(meta, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [2] }); + const container = new ComposableRevealContainer(meta, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [2] }); // Add only one element container.create({ token: 'token-1' }); const host = document.createElement('div'); @@ -164,7 +164,7 @@ describe("Reveal Composable Container Class", () => { const styles = { base: { color: 'blue' } }; const errorTextStyles = { base: { color: 'red' } }; const meta = { ...testMetaData }; - const container = new ComposableRevealContainer(meta, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [2], styles, errorTextStyles }); + const container = new ComposableRevealContainer(meta, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [2], styles, errorTextStyles }); container.create({ token: 'token-1' }); container.create({ token: 'token-2' }); const host = document.createElement('div'); @@ -184,7 +184,7 @@ describe("Reveal Composable Container Class", () => { test('mount() inside shadow DOM should emit HEIGHT event via postMessage', () => { const meta = { ...testMetaData }; - const container = new ComposableRevealContainer(meta, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(meta, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: 'token-1' }); // Shadow host setup const shadowHost = document.createElement('div'); @@ -252,7 +252,7 @@ describe("Reveal Composable Container Class", () => { }); test("on container mounted call back",()=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -268,7 +268,7 @@ describe("Reveal Composable Container Class", () => { testRevealContainer.mount('#container'); }); // test("on container mounted call back 5",()=>{ -// const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); +// const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); // testRevealContainer.create({ // token: "token", // }); @@ -295,7 +295,7 @@ describe("Reveal Composable Container Class", () => { // }); // test("on container mounted else call back",()=>{ -// const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); +// const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); // testRevealContainer.create({ // token: "1815-6223-1073-1425", // }); @@ -326,7 +326,7 @@ describe("Reveal Composable Container Class", () => { // emitCb({error:{code:404,description:"Not Found"}}); // }); // test("on container mounted else call back 1",()=>{ -// const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); +// const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); // testRevealContainer.create({ // token: "1815-6223-1073-1425", // }); @@ -356,7 +356,7 @@ describe("Reveal Composable Container Class", () => { // emitCb({"success":[{token:"1815-6223-1073-1425"}]}); // }); test("reveal before skyflow frame ready event",async ()=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -389,7 +389,7 @@ describe("Reveal Composable Container Class", () => { await expect(res).resolves.toEqual({"success":[{token:"1815-6223-1073-1425"}]}); }); test("reveal before skyflow frame ready event, Error case",async ()=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -438,7 +438,7 @@ describe("Reveal Composable Container Class", () => { getSkyflowBearerToken: getBearerTokenFail, }; - const testRevealContainer = new ComposableRevealContainer(clientDataFail, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientDataFail, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -456,7 +456,7 @@ describe("Reveal Composable Container Class", () => { /// frame ready event test("reveal before skyflow frame ready event",async ()=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -489,7 +489,7 @@ describe("Reveal Composable Container Class", () => { await expect(res).resolves.toEqual({"success":[{token:"1815-6223-1073-1425"}]}); }); test("reveal before skyflow frame ready event, Error case",async ()=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -538,7 +538,7 @@ describe("Reveal Composable Container Class", () => { getSkyflowBearerToken: getBearerTokenFail, }; - const testRevealContainer = new ComposableRevealContainer(clientDataFail, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientDataFail, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); window.dispatchEvent(new MessageEvent('message', { @@ -561,7 +561,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when elment is empty when skyflow ready",(done)=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); @@ -580,7 +580,7 @@ describe("Reveal Composable Container Class", () => { }) }); test("reveal when elment is empty when skyflow frame not ready",(done)=>{ - const testRevealContainer = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.reveal().catch((error) => { @@ -605,7 +605,7 @@ describe("Reveal Composable Container Class", () => { getSkyflowBearerToken: getBearerTokenFail, }; - const testRevealContainer = new ComposableRevealContainer(clientDataFail, [], { logLevel: LogLevel.ERROR,env:Env.PROD }, { + const testRevealContainer = new ComposableRevealContainer(clientDataFail, { logLevel: LogLevel.ERROR,env:Env.PROD }, { layout:[1] }); testRevealContainer.create({ @@ -622,7 +622,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame not ready - ignores MOUNTED message from wrong origin", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); const res = container.reveal(); @@ -659,7 +659,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame not ready - inner listener rejects when revealData has errors", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); const res = container.reveal(); @@ -687,7 +687,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame not ready - inner listener ignores REVEAL_RESPONSE_READY from wrong origin", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); const res = container.reveal(); @@ -728,7 +728,7 @@ describe("Reveal Composable Container Class", () => { // #isComposableFrameReady is set to true by dispatching MOUNTED before calling reveal() test("reveal when frame already ready - resolves with success data", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); // Set #isComposableFrameReady = true before calling reveal() window.dispatchEvent(new MessageEvent('message', { @@ -750,7 +750,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame already ready - rejects with error data", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); window.dispatchEvent(new MessageEvent('message', { data: { type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid } @@ -771,7 +771,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame already ready - ignores message from wrong origin", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); window.dispatchEvent(new MessageEvent('message', { data: { type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid } @@ -802,7 +802,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame already ready - ignores message with wrong type", async () => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); window.dispatchEvent(new MessageEvent('message', { data: { type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid } @@ -838,7 +838,7 @@ describe("Reveal Composable Container Class", () => { }); const clientDataFail = { ...clientData, getSkyflowBearerToken: getBearerTokenFail }; - const container = new ComposableRevealContainer(clientDataFail, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientDataFail, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); container.create({ token: "1815-6223-1073-1425" }); window.dispatchEvent(new MessageEvent('message', { data: { type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid } @@ -850,7 +850,7 @@ describe("Reveal Composable Container Class", () => { }); test("reveal when frame already ready - throws error when no elements in container", (done) => { - const container = new ComposableRevealContainer(clientData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); + const container = new ComposableRevealContainer(clientData, { logLevel: LogLevel.ERROR, env: Env.PROD }, { layout: [1] }); window.dispatchEvent(new MessageEvent('message', { data: { type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + mockUuid } })); diff --git a/tests/core/external/reveal/reveal-composable-element.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-element.test.js similarity index 92% rename from tests/core/external/reveal/reveal-composable-element.test.js rename to packages/skyflow-js/tests/core/external/reveal/reveal-composable-element.test.js index ab8a3a20c..49667c307 100644 --- a/tests/core/external/reveal/reveal-composable-element.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-element.test.js @@ -2,12 +2,12 @@ Copyright (c) 2022 Skyflow, Inc. */ import { LogLevel,Env } from "../../../../src/utils/common"; -import { ELEMENT_EVENTS_TO_IFRAME, FRAME_REVEAL, ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, CUSTOM_ERROR_MESSAGES} from "../../../../src/core/constants"; -import SkyflowContainer from '../../../../src/core/external/skyflow-container'; -import Client from '../../../../src/client'; -import EventEmitter from "../../../../src/event-emitter"; -import * as busEvents from '../../../../src/utils/bus-events'; -import ComposableRevealInternalElement from "../../../../src/core/external/reveal/composable-reveal-internal"; +import { ELEMENT_EVENTS_TO_IFRAME, FRAME_REVEAL, ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, CUSTOM_ERROR_MESSAGES} from "@core/constants"; +import SkyflowContainer from '../../../../src/external/skyflow-container'; +import Client from '@core/client'; +import EventEmitter from "@core/event-emitter"; +import * as busEvents from '@core/utils/bus-events'; +import ComposableRevealInternalElement from "../../../../src/external/reveal/composable-reveal-internal"; import bus from "framebus"; import { JSDOM } from 'jsdom'; @@ -16,7 +16,7 @@ import { ComposableRevealElement, ErrorType, EventName, RedactionType } from ".. busEvents.getAccessToken = jest.fn(() => Promise.reject('access token')); const mockUuid = '1234'; -jest.mock('../../../../src/libs/uuid',()=>({ +jest.mock('@core/libs/uuid',()=>({ __esModule: true, default:jest.fn(()=>(mockUuid)), })); @@ -27,7 +27,7 @@ const getBearerToken = jest.fn(); const groupEmittFn = jest.fn(); let groupOnCb; -jest.mock('../../../../src/libs/jss-styles', () => { +jest.mock('@core/libs/jss-styles', () => { return { __esModule: true, default: jest.fn(), @@ -38,14 +38,14 @@ jest.mock('../../../../src/libs/jss-styles', () => { }) }; }); -jest.mock('../../../../src/core/external/skyflow-container', () => { +jest.mock('../../../../src/external/skyflow-container', () => { return { __esModule: true, default: jest.fn(), } }) -// jest.mock('../../../../src/core/external/reveal/composable-reveal-internal') +// jest.mock('../../../../src/external/reveal/composable-reveal-internal') // bus.on = _on; // bus.target = jest.fn().mockReturnValue({ diff --git a/tests/core/external/reveal/reveal-composable-internal.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-internal.test.js similarity index 98% rename from tests/core/external/reveal/reveal-composable-internal.test.js rename to packages/skyflow-js/tests/core/external/reveal/reveal-composable-internal.test.js index 4d79828cd..59b40c6fc 100644 --- a/tests/core/external/reveal/reveal-composable-internal.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-internal.test.js @@ -2,23 +2,23 @@ Copyright (c) 2022 Skyflow, Inc. */ import { LogLevel,Env, ErrorType } from "../../../../src/utils/common"; -import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_REVEAL, ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, CUSTOM_ERROR_MESSAGES} from "../../../../src/core/constants"; -import RevealElement from "../../../../src/core/external/reveal/reveal-element"; -import SkyflowContainer from '../../../../src/core/external/skyflow-container'; -import Client from '../../../../src/client'; -import ComposableRevealInternalElement from "../../../../src/core/external/reveal/composable-reveal-internal"; -import * as busEvents from '../../../../src/utils/bus-events'; +import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_REVEAL, ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, CUSTOM_ERROR_MESSAGES} from "@core/constants"; +import RevealElement from "../../../../src/external/reveal/reveal-element"; +import SkyflowContainer from '../../../../src/external/skyflow-container'; +import Client from '@core/client'; +import ComposableRevealInternalElement from "../../../../src/external/reveal/composable-reveal-internal"; +import * as busEvents from '@core/utils/bus-events'; import bus from "framebus"; import { JSDOM } from 'jsdom'; -import EventEmitter from "../../../../src/event-emitter"; +import EventEmitter from "@core/event-emitter"; import { error } from "console"; -import properties from "../../../../src/properties"; +import properties from "@core/properties"; busEvents.getAccessToken = jest.fn(() => Promise.reject('access token')); const mockUuid = '1234'; const elementId = 'id'; -jest.mock('../../../../src/libs/uuid',()=>({ +jest.mock('@core/libs/uuid',()=>({ __esModule: true, default:jest.fn(()=>(mockUuid)), })); @@ -40,7 +40,7 @@ const groupEmiitter = { groupOnCb = cb; }) } -jest.mock('../../../../src/libs/jss-styles', () => { +jest.mock('@core/libs/jss-styles', () => { return { __esModule: true, default: jest.fn(), @@ -51,7 +51,7 @@ jest.mock('../../../../src/libs/jss-styles', () => { }) }; }); -jest.mock('../../../../src/core/external/skyflow-container', () => { +jest.mock('../../../../src/external/skyflow-container', () => { return { __esModule: true, default: jest.fn(), diff --git a/tests/core/external/reveal/reveal-container.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js similarity index 92% rename from tests/core/external/reveal/reveal-container.test.js rename to packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js index 660b5ee65..caeee6f71 100644 --- a/tests/core/external/reveal/reveal-container.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js @@ -1,20 +1,20 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import RevealContainer from "../../../../src/core/external/reveal/reveal-container"; -import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_CONTAINER, ELEMENT_EVENTS_TO_IFRAME, REVEAL_FRAME_CONTROLLER, REVEAL_TYPES } from "../../../../src/core/constants"; +import RevealContainer from "../../../../src/external/reveal/reveal-container"; +import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_CONTAINER, ELEMENT_EVENTS_TO_IFRAME, REVEAL_FRAME_CONTROLLER, REVEAL_TYPES } from "@core/constants"; import bus from "framebus"; import { LogLevel,Env } from "../../../../src/utils/common"; -import RevealElement from "../../../../src/core/external/reveal/reveal-element"; -import * as iframerUtils from '../../../../src/iframe-libs/iframer'; -import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import RevealElement from "../../../../src/external/reveal/reveal-element"; +import * as iframerUtils from '@core/iframe-libs/iframer'; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { parameterizedString } from "../../../../src/utils/logs-helper"; -import SkyflowError from "../../../../src/libs/skyflow-error"; -import logs from "../../../../src/utils/logs"; +import SkyflowError from "@core/errors"; +import logs from "@core/utils/logs"; iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); const mockUuid = '1234'; -jest.mock('../../../../src/libs/uuid',()=>({ +jest.mock('@core/libs/uuid',()=>({ __esModule: true, default:jest.fn(()=>(mockUuid)), })); @@ -113,7 +113,7 @@ describe("Reveal Container Class", () => { }, }; test("reveal should throw error with no elements", (done) => { - const container = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const container = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); container.reveal().catch((error) => { done(); expect(error).toBeDefined(); @@ -123,7 +123,7 @@ describe("Reveal Container Class", () => { }) }); - const testRevealContainer = new RevealContainer(testMetaData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR,env:Env.PROD }); test("constructor", () => { expect(testRevealContainer).toBeInstanceOf(RevealContainer); expect(testRevealContainer).toBeInstanceOf(Object); @@ -168,7 +168,7 @@ describe("Reveal Container Class", () => { } }); test("on container mounted call back",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -202,7 +202,7 @@ describe("Reveal Container Class", () => { emitCb({error:{code:404,description:"Not Found"}}); }); test("on container mounted call back 2",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -231,7 +231,7 @@ describe("Reveal Container Class", () => { emitCb({"success":[{token:"1815-6223-1073-1425"}]}); }); test("on container mounted call back 3",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -266,7 +266,7 @@ describe("Reveal Container Class", () => { emitCb({"success":[{token:"1815-6223-1073-1425"}]}); }); test("on container mounted call back 4",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "123", }); @@ -303,7 +303,7 @@ describe("Reveal Container Class", () => { emitCb({"success":[{token:"1815-6223-1073-1425"}]}); }); test("on container mounted call back 5",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "token", }); @@ -330,7 +330,7 @@ describe("Reveal Container Class", () => { }); test("on container mounted else call back",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -361,7 +361,7 @@ describe("Reveal Container Class", () => { emitCb({error:{code:404,description:"Not Found"}}); }); test("on container mounted else call back 1",()=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -391,7 +391,7 @@ describe("Reveal Container Class", () => { emitCb({"success":[{token:"1815-6223-1073-1425"}]}); }); test("reveal before skyflow frame ready event",()=>{ - const testRevealContainer = new RevealContainer(clientData2, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData2, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -416,7 +416,7 @@ describe("Reveal Container Class", () => { emitCb({"success":[{token:"1815-6223-1073-1425"}]}); }); test("reveal before skyflow frame ready when element have error",(done)=>{ - const testRevealContainer = new RevealContainer(clientData2, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData2, { logLevel: LogLevel.ERROR,env:Env.PROD }); var element = testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -435,7 +435,7 @@ describe("Reveal Container Class", () => { }) }); test("reveal before skyflow frame ready",(done)=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); var element = testRevealContainer.create({ token: "1815-6223-1073-1425", }); @@ -454,7 +454,7 @@ describe("Reveal Container Class", () => { }) }); test("reveal when elment is empty when skyflow ready",(done)=>{ - const testRevealContainer = new RevealContainer(clientData2, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData2, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.reveal().catch((error) => { done(); @@ -465,7 +465,7 @@ describe("Reveal Container Class", () => { }) }); test("reveal when elment is empty when skyflow frame not ready",(done)=>{ - const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); testRevealContainer.reveal().catch((error) => { done(); @@ -476,7 +476,7 @@ describe("Reveal Container Class", () => { }) }); // test("file render call",async ()=>{ - // const testRevealContainer = new RevealContainer(clientData, {}, { logLevel: LogLevel.ERROR,env:Env.PROD }); + // const testRevealContainer = new RevealContainer(clientData, { logLevel: LogLevel.ERROR,env:Env.PROD }); // const { window } = new JSDOM('
    '); // global.document = window.document; // let ele = document.createElement('div'); diff --git a/tests/core/external/reveal/reveal-container.test.ts b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts similarity index 93% rename from tests/core/external/reveal/reveal-container.test.ts rename to packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts index dee56702c..f89524bc5 100644 --- a/tests/core/external/reveal/reveal-container.test.ts +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts @@ -1,30 +1,30 @@ /* Copyright (c) 2025 Skyflow, Inc. */ -import RevealContainer from "../../../../src/core/external/reveal/reveal-container"; +import RevealContainer from "../../../../src/external/reveal/reveal-container"; import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_CONTAINER, ELEMENT_EVENTS_TO_IFRAME, REVEAL_FRAME_CONTROLLER, REVEAL_TYPES, -} from "../../../../src/core/constants"; +} from "@core/constants"; import bus from "framebus"; import { LogLevel, Env } from "../../../../src/utils/common"; -import RevealElement from "../../../../src/core/external/reveal/reveal-element"; -import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import RevealElement from "../../../../src/external/reveal/reveal-element"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { parameterizedString } from "../../../../src/utils/logs-helper"; -import SkyflowError from "../../../../src/libs/skyflow-error"; -import logs from "../../../../src/utils/logs"; -import { Metadata } from "../../../../src/core/internal/internal-types"; -import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import SkyflowError from "@core/errors"; +import logs from "@core/utils/logs"; +import { Metadata } from "../../../../src/internal/internal-types"; +import SkyflowContainer from "../../../../src/external/skyflow-container"; import { ContainerType, RevealResponse } from "../../../../src/index-node"; import { ISkyflow } from "../../../../src/skyflow"; import assert, { AssertionError, fail } from "assert"; -jest.mock("../../../../src/iframe-libs/iframer", () => { +jest.mock("@core/iframe-libs/iframer", () => { const actualModule = jest.requireActual( - "../../../../src/iframe-libs/iframer" + "@core/iframe-libs/iframer" ); const mockedModule = { ...actualModule }; mockedModule.__esModule = true; @@ -33,7 +33,7 @@ jest.mock("../../../../src/iframe-libs/iframer", () => { }); const mockUuid = "1234"; -jest.mock("../../../../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => mockUuid), })); @@ -83,12 +83,12 @@ const testRecord = { }, }; -const testRevealContainer1 = new RevealContainer(testMetaData, [], { +const testRevealContainer1 = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); -const testRevealContainer2 = new RevealContainer(testMetaData2, [], { +const testRevealContainer2 = new RevealContainer(testMetaData2, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -115,7 +115,7 @@ describe("Reveal Container Class", () => { }); test("reveal should throw error with no elements", (done) => { - const container = new RevealContainer(testMetaData, [], { + const container = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -183,7 +183,7 @@ describe("Reveal Container Class", () => { }); test("handle reveal errors with 404 response", async () => { - const testRevealContainer = new RevealContainer(testMetaData, [], { + const testRevealContainer = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -214,7 +214,7 @@ describe("Reveal Container Class", () => { }); test("handle successful reveal when called before mounting", async () => { - const testRevealContainer = new RevealContainer(testMetaData, [], { + const testRevealContainer = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -245,7 +245,7 @@ describe("Reveal Container Class", () => { }); test("frame controller ready event correctly", async () => { - const testRevealContainer = new RevealContainer(testMetaData, [], { + const testRevealContainer = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -273,7 +273,7 @@ describe("Reveal Container Class", () => { }); test("on container mounted else call back", async () => { - const testRevealContainer = new RevealContainer(testMetaData, [], { + const testRevealContainer = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -302,7 +302,7 @@ describe("Reveal Container Class", () => { }); test("on container mounted else call back 1", async () => { - const testRevealContainer = new RevealContainer(testMetaData, [], { + const testRevealContainer = new RevealContainer(testMetaData, { logLevel: LogLevel.ERROR, env: Env.PROD, }); @@ -330,7 +330,7 @@ describe("Reveal Container Class", () => { }); test("reveal before skyflow frame ready event", async () => { - const testRevealContainer = new RevealContainer(testMetaData2, [], { + const testRevealContainer = new RevealContainer(testMetaData2, { logLevel: LogLevel.ERROR, env: Env.PROD, }); diff --git a/tests/core/external/reveal/reveal-element.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.js similarity index 98% rename from tests/core/external/reveal/reveal-element.test.js rename to packages/skyflow-js/tests/core/external/reveal/reveal-element.test.js index ccee593fa..e6fc420e7 100644 --- a/tests/core/external/reveal/reveal-element.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.js @@ -2,24 +2,24 @@ Copyright (c) 2022 Skyflow, Inc. */ import { LogLevel,Env, ErrorType } from "../../../../src/utils/common"; -import { ELEMENT_EVENTS_TO_IFRAME, FRAME_REVEAL, ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, CUSTOM_ERROR_MESSAGES} from "../../../../src/core/constants"; -import RevealElement from "../../../../src/core/external/reveal/reveal-element"; -import SkyflowContainer from '../../../../src/core/external/skyflow-container'; -import Client from '../../../../src/client'; +import { ELEMENT_EVENTS_TO_IFRAME, FRAME_REVEAL, ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, CUSTOM_ERROR_MESSAGES} from "@core/constants"; +import RevealElement from "../../../../src/external/reveal/reveal-element"; +import SkyflowContainer from '../../../../src/external/skyflow-container'; +import Client from '@core/client'; -import * as busEvents from '../../../../src/utils/bus-events'; +import * as busEvents from '@core/utils/bus-events'; import bus from "framebus"; import { JSDOM } from 'jsdom'; -import EventEmitter from "../../../../src/event-emitter"; +import EventEmitter from "@core/event-emitter"; busEvents.getAccessToken = jest.fn(() => Promise.reject('access token')); const mockUuid = '1234'; const elementId = 'id'; -jest.mock('../../../../src/libs/uuid',()=>({ +jest.mock('@core/libs/uuid',()=>({ __esModule: true, default:jest.fn(()=>(mockUuid)), })); @@ -36,7 +36,7 @@ const groupEmiitter = { groupOnCb = cb; }) } -jest.mock('../../../../src/libs/jss-styles', () => { +jest.mock('@core/libs/jss-styles', () => { return { __esModule: true, default: jest.fn(), @@ -47,7 +47,7 @@ jest.mock('../../../../src/libs/jss-styles', () => { }) }; }); -jest.mock('../../../../src/core/external/skyflow-container', () => { +jest.mock('../../../../src/external/skyflow-container', () => { return { __esModule: true, default: jest.fn(), diff --git a/tests/core/external/reveal/reveal-element.test.ts b/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts similarity index 97% rename from tests/core/external/reveal/reveal-element.test.ts rename to packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts index 9d181f13c..b1a6f5c5d 100644 --- a/tests/core/external/reveal/reveal-element.test.ts +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts @@ -8,21 +8,21 @@ import { ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, - ElementType, + BaseElementType, CUSTOM_ERROR_MESSAGES, -} from "../../../../src/core/constants"; -import RevealElement from "../../../../src/core/external/reveal/reveal-element"; -import SkyflowContainer from "../../../../src/core/external/skyflow-container"; -import Client from "../../../../src/client"; +} from "@core/constants"; +import RevealElement from "../../../../src/external/reveal/reveal-element"; +import SkyflowContainer from "../../../../src/external/skyflow-container"; +import Client from "@core/client"; -import * as busEvents from "../../../../src/utils/bus-events"; +import * as busEvents from "@core/utils/bus-events"; import bus from "framebus"; import { JSDOM } from "jsdom"; -import { Metadata } from "../../../../src/core/internal/internal-types"; +import { Metadata } from "../../../../src/internal/internal-types"; import { ContainerType, ISkyflow } from "../../../../src/skyflow"; -import { IRevealElementInput } from "../../../../src/core/external/reveal/reveal-container"; -import EventEmitter from "../../../../src/event-emitter"; +import { IRevealElementInput } from "../../../../src/external/reveal/reveal-container"; +import EventEmitter from "@core/event-emitter"; import { ErrorType, RevealElementInput } from "../../../../src/index-node"; jest @@ -31,7 +31,7 @@ jest const mockUuid = "1234"; const elementId = "id"; -jest.mock("../../../../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => mockUuid), })); @@ -54,7 +54,7 @@ const groupEmiitter: EventEmitter = { resetEvents: jest.fn(), }; -jest.mock("../../../../src/libs/jss-styles", () => { +jest.mock("@core/libs/jss-styles", () => { return { __esModule: true, default: jest.fn(), @@ -66,7 +66,7 @@ jest.mock("../../../../src/libs/jss-styles", () => { }; }); -jest.mock("../../../../src/core/external/skyflow-container", () => { +jest.mock("../../../../src/external/skyflow-container", () => { return { __esModule: true, default: jest.fn(), diff --git a/packages/skyflow-js/tests/core/external/skyflow-container.test.ts b/packages/skyflow-js/tests/core/external/skyflow-container.test.ts new file mode 100644 index 000000000..d959eb782 --- /dev/null +++ b/packages/skyflow-js/tests/core/external/skyflow-container.test.ts @@ -0,0 +1,74 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import SkyflowContainer from "../../../src/external/skyflow-container"; +import Client from "@core/client"; +import * as iframerUtils from "@core/iframe-libs/iframer"; +import { Env, LogLevel } from "../../../src/utils/common"; +import { ISkyflow } from "../../../src/skyflow"; + +jest + .spyOn(iframerUtils, "getIframeSrc") + .mockImplementation(() => "https://google.com"); + +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" }, +}; + +const metaData = { + uuid: "123", + clientDomain: "http://abc.com", +}; + +// Guards the pre-split serialized shape of the SkyflowContainer that rides the +// element iframe `src` URL. `client`/`containerId`/`context` are `protected` +// (enumerable at runtime) for subclass access; without `toJSON()` they leak the +// whole client config into every element URL. See SK-3041 metadata regression. +describe("SkyflowContainer metadata serialization", () => { + beforeEach(() => { + jest.spyOn(bus, "target").mockReturnValue({ + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + } as any); + }); + + afterEach(() => { + jest.restoreAllMocks(); + document.body.innerHTML = ""; + }); + + it("serializes only isControllerFrameReady, not client/containerId/context", () => { + const client = new Client(skyflowConfig, metaData); + const container = new SkyflowContainer(client, { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + // The live object still exposes state for client-side readiness reads. + expect(container.isControllerFrameReady).toBe(false); + + // But JSON.stringify (the iframe-URL path) emits only the ready flag. + const serialized = JSON.parse(JSON.stringify(container)); + expect(serialized).toEqual({ isControllerFrameReady: false }); + expect(serialized).not.toHaveProperty("client"); + expect(serialized).not.toHaveProperty("containerId"); + expect(serialized).not.toHaveProperty("context"); + }); + + it("reflects the live isControllerFrameReady value when serialized", () => { + const client = new Client(skyflowConfig, metaData); + const container = new SkyflowContainer(client, { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + container.isControllerFrameReady = true; + const serialized = JSON.parse(JSON.stringify(container)); + expect(serialized).toEqual({ isControllerFrameReady: true }); + }); +}); diff --git a/tests/core/external/threeds/threeds.test.js b/packages/skyflow-js/tests/core/external/threeds/threeds.test.js similarity index 97% rename from tests/core/external/threeds/threeds.test.js rename to packages/skyflow-js/tests/core/external/threeds/threeds.test.js index 689704f84..24374ff79 100644 --- a/tests/core/external/threeds/threeds.test.js +++ b/packages/skyflow-js/tests/core/external/threeds/threeds.test.js @@ -1,5 +1,5 @@ -import ThreeDS from '../../../../src/core/external/threeds/threeds'; -import SkyflowError from '../../../../src/libs/skyflow-error'; +import ThreeDS from '../../../../src/external/threeds/threeds'; +import SkyflowError from '@core/errors'; describe('test 3DS helperFunction', ()=>{ let originalCreateElement; diff --git a/tests/core/internal/composable-frame-element-init.test.js b/packages/skyflow-js/tests/core/internal/composable-frame-element-init.test.js similarity index 98% rename from tests/core/internal/composable-frame-element-init.test.js rename to packages/skyflow-js/tests/core/internal/composable-frame-element-init.test.js index 6d22cb620..50ed4fbe4 100644 --- a/tests/core/internal/composable-frame-element-init.test.js +++ b/packages/skyflow-js/tests/core/internal/composable-frame-element-init.test.js @@ -1,17 +1,17 @@ -// import FrameElementInit from '../../../src/core/internal/frame-element-init'; -import RevealComposableFrameElementInit from '../../../src/core/internal/composable-frame-element-init'; -import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_REVEAL, ELEMENT_EVENTS_TO_CLIENT, FRAME_REVEAL, REVEAL_TYPES } from '../../../src/core/constants'; +// import FrameElementInit from '../../../src/internal/frame-element-init'; +import RevealComposableFrameElementInit from '../../../src/internal/composable-frame-element-init'; +import { ELEMENT_EVENTS_TO_IFRAME, COMPOSABLE_REVEAL, ELEMENT_EVENTS_TO_CLIENT, FRAME_REVEAL, REVEAL_TYPES } from '@core/constants'; import bus from 'framebus'; -import SkyflowError from '../../../src/libs/skyflow-error'; -import { fetchRecordsByTokenIdComposable, formatRecordsForClientComposable } from '../../../src/core-utils/reveal'; -import properties from '../../../src/properties'; +import SkyflowError from '@core/errors'; +import { fetchRecordsByTokenIdComposable, formatRecordsForClientComposable } from '../../../src/api-utils/reveal'; +import properties from '@core/properties'; // Create a mock function that can be controlled per test const mockFetchRecordsByTokenIdComposable = jest.fn(); // mock fetchRecordsByTokenIdComposable with a jest.fn() so we can control it per test -jest.mock('../../../src/core-utils/reveal', () => { - const actual = jest.requireActual('../../../src/core-utils/reveal'); +jest.mock('../../../src/api-utils/reveal', () => { + const actual = jest.requireActual('../../../src/api-utils/reveal'); return { ...actual, fetchRecordsByTokenIdComposable: (...args) => mockFetchRecordsByTokenIdComposable(...args), diff --git a/tests/core/internal/frame-element-init.additional.test.js b/packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js similarity index 97% rename from tests/core/internal/frame-element-init.additional.test.js rename to packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js index a2b1f89ac..273a2b435 100644 --- a/tests/core/internal/frame-element-init.additional.test.js +++ b/packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js @@ -1,5 +1,5 @@ // Mock Client so internal FrameElementInit imports use a controllable request fn. -jest.mock('../../../src/client', () => { +jest.mock('@core/client', () => { const mockClientRequest = jest.fn().mockResolvedValue({ upload: 'ok' }); class Client { constructor(config, meta) { @@ -14,12 +14,16 @@ jest.mock('../../../src/client', () => { } return { __esModule: true, default: Client, mockClientRequest }; }); -// Mock collect helpers BEFORE importing FrameElementInit so internal references use mocks -jest.mock('../../../src/core-utils/collect', () => { - const constructElementsInsertReq = jest.fn((insertObj, updateObj) => [ +// Mock collect helpers BEFORE importing FrameElementInit so internal references use mocks. +// constructElementsInsertReq now lives in @core/api-utils/collect, so mock it there. +jest.mock('@core/api-utils/collect', () => ({ + __esModule: true, + constructElementsInsertReq: jest.fn((insertObj, updateObj) => [ { insertRecords: insertObj }, { updateRecords: Object.entries(updateObj).map(([skyflowID, record]) => ({ skyflowID, ...record })) }, - ]); + ]), +})); +jest.mock('../../../src/api-utils/collect', () => { const constructInsertRecordRequest = jest.fn((finalInsertRecords) => { if (finalInsertRecords && typeof finalInsertRecords === 'object' && !Array.isArray(finalInsertRecords)) { return Object.entries(finalInsertRecords).map(([table, fields]) => ({ table, fields })); @@ -30,29 +34,28 @@ jest.mock('../../../src/core-utils/collect', () => { const updateRecordsBySkyflowIDComposable = jest.fn(() => Promise.resolve({ records: [{ id: 'update1' }] })); return { __esModule: true, - constructElementsInsertReq, constructInsertRecordRequest, insertDataInCollect, updateRecordsBySkyflowIDComposable, }; }); -import FrameElementInit from '../../../src/core/internal/frame-element-init'; -import SkyflowError from '../../../src/libs/skyflow-error'; -import { ELEMENTS } from '../../../src/core/constants'; -import logs from '../../../src/utils/logs'; +import FrameElementInit from '../../../src/internal/frame-element-init'; +import SkyflowError from '@core/errors'; +import { ELEMENTS } from '@core/constants'; +import logs from '@core/utils/logs'; import { parameterizedString } from '../../../src/utils/logs-helper'; import * as helpers from '../../../src/utils/helpers'; -import Client, { mockClientRequest } from '../../../src/client'; -import { ELEMENT_EVENTS_TO_IFRAME, COLLECT_TYPES } from '../../../src/core/constants'; +import Client, { mockClientRequest } from '@core/client'; +import { ELEMENT_EVENTS_TO_IFRAME, COLLECT_TYPES } from '@core/constants'; +import { constructElementsInsertReq } from '@core/api-utils/collect'; import { - constructElementsInsertReq, constructInsertRecordRequest, insertDataInCollect, updateRecordsBySkyflowIDComposable, -} from '../../../src/core-utils/collect'; +} from '../../../src/api-utils/collect'; import { ErrorType } from '../../../src/index-node'; // Mock element-options to bypass complex row merging logic that expects prior group structure -jest.mock('../../../src/libs/element-options', () => ({ +jest.mock('@core/libs/element-options', () => ({ validateAndSetupGroupOptions: (oldGroup, newGroup) => newGroup || oldGroup || { rows: [] }, getValueAndItsUnit: (v) => [v || ''], })); @@ -795,19 +798,22 @@ describe('FrameElementInit extended unit tests', () => { ); }); - test('handleCollectCall: COMPOSABLE_CONTAINER message sets client without error', async () => { + test('handleCollectCall: COMPOSABLE_CONTAINER message is a no-op and does not build a client', async () => { + // The frame builds its client per-request from clientConfig in + // dispatchCollectRequest, so a COMPOSABLE_CONTAINER window message no longer + // constructs a client here — it must be handled without error. const instance = new FrameElementInit(); const spyFromJSON = jest.spyOn(Client, 'fromJSON'); const clientConfigPayload = { config: { vaultURL: 'https://vault.url', vaultID: 'vaultX' } }; - instance['handleCollectCall']({ + expect(() => instance['handleCollectCall']({ origin: 'http://localhost.com', data: { name: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + instance.containerId, client: clientConfigPayload, }, - }); + })).not.toThrow(); await flushPromises(); - expect(spyFromJSON).toHaveBeenCalled(); + expect(spyFromJSON).not.toHaveBeenCalled(); }); // ===== Additional tokenize branch coverage (lines ~315-469) ===== diff --git a/tests/core/internal/frame-element-init.fulfilled-errors.test.js b/packages/skyflow-js/tests/core/internal/frame-element-init.fulfilled-errors.test.js similarity index 87% rename from tests/core/internal/frame-element-init.fulfilled-errors.test.js rename to packages/skyflow-js/tests/core/internal/frame-element-init.fulfilled-errors.test.js index 46215c14e..26fb593d2 100644 --- a/tests/core/internal/frame-element-init.fulfilled-errors.test.js +++ b/packages/skyflow-js/tests/core/internal/frame-element-init.fulfilled-errors.test.js @@ -1,15 +1,19 @@ -import FrameElementInit from '../../../src/core/internal/frame-element-init'; -import { ELEMENTS } from '../../../src/core/constants'; -import SkyflowError from '../../../src/libs/skyflow-error'; +import FrameElementInit from '../../../src/internal/frame-element-init'; +import { ELEMENTS } from '@core/constants'; +import SkyflowError from '@core/errors'; import * as helpers from '../../../src/utils/helpers'; -import { constructElementsInsertReq, constructInsertRecordRequest, insertDataInCollect, updateRecordsBySkyflowIDComposable } from '../../../src/core-utils/collect'; +import { constructElementsInsertReq } from '@core/api-utils/collect'; +import { constructInsertRecordRequest, insertDataInCollect, updateRecordsBySkyflowIDComposable } from '../../../src/api-utils/collect'; -// Reuse existing mocks from additional test by mocking modules again (Jest will hoist mocks) -jest.mock('../../../src/core-utils/collect', () => { - const constructElementsInsertReq = jest.fn((insertObj, updateObj) => [ +// constructElementsInsertReq now lives in @core/api-utils/collect; mock it there. +jest.mock('@core/api-utils/collect', () => ({ + __esModule: true, + constructElementsInsertReq: jest.fn((insertObj, updateObj) => [ { insertRecords: insertObj }, { updateRecords: Object.entries(updateObj).map(([skyflowID, record]) => ({ skyflowID, ...record })) }, - ]); + ]), +})); +jest.mock('../../../src/api-utils/collect', () => { const constructInsertRecordRequest = jest.fn((finalInsertRecords) => { if (finalInsertRecords && typeof finalInsertRecords === 'object' && !Array.isArray(finalInsertRecords)) { return Object.entries(finalInsertRecords).map(([table, fields]) => ({ table, fields })); @@ -20,7 +24,6 @@ jest.mock('../../../src/core-utils/collect', () => { const updateRecordsBySkyflowIDComposable = jest.fn(() => Promise.resolve({ records: [{ id: 'update1' }] })); return { __esModule: true, - constructElementsInsertReq, constructInsertRecordRequest, insertDataInCollect, updateRecordsBySkyflowIDComposable, diff --git a/tests/core/internal/frame-element-init.test.js b/packages/skyflow-js/tests/core/internal/frame-element-init.test.js similarity index 97% rename from tests/core/internal/frame-element-init.test.js rename to packages/skyflow-js/tests/core/internal/frame-element-init.test.js index f9a21d74b..7835e3307 100644 --- a/tests/core/internal/frame-element-init.test.js +++ b/packages/skyflow-js/tests/core/internal/frame-element-init.test.js @@ -1,10 +1,10 @@ -import FrameElementInit from '../../../src/core/internal/frame-element-init'; -import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, ElementType, COLLECT_TYPES } from '../../../src/core/constants'; +import FrameElementInit from '../../../src/internal/frame-element-init'; +import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, BaseElementType, FileElementType, COLLECT_TYPES } from '@core/constants'; import bus from 'framebus'; -import SkyflowError from '../../../src/libs/skyflow-error'; +import SkyflowError from '@core/errors'; import * as helpers from '../../../src/utils/helpers'; -import Client from '../../../src/client'; -import IFrameFormElement from '../../../src/core/internal/iframe-form'; +import Client from '@core/client'; +import IFrameFormElement from '@core/internal/iframe-form'; import { ErrorType } from '../../../src/index-node'; // Helper to flush pending microtasks (Promise.allSettled resolution) deterministically @@ -37,8 +37,8 @@ const mockFileList = mockFile1; // } // } // }; -// jest.mock('../../../src/core/internal/iframe-form', () => { -// const actual = jest.requireActual('../../../src/core/internal/iframe-form'); +// jest.mock('@core/internal/iframe-form', () => { +// const actual = jest.requireActual('@core/internal/iframe-form'); // return { // __esModule: true, @@ -109,7 +109,7 @@ const element = { ...stylesOptions }, { - elementType: ElementType.MULTI_FILE_INPUT, + elementType: FileElementType.MULTI_FILE_INPUT, elementName: `element:MULTI_FILE_INPUT:123`, table: 'patients', column: 'file_uploads', @@ -621,7 +621,7 @@ describe('FrameElementInit Additional Test Cases', () => { ...stylesOptions }, { - elementType: ElementType.MULTI_FILE_INPUT, + elementType: FileElementType.MULTI_FILE_INPUT, elementName: `element:MULTI_FILE_INPUT:123`, table: 'patients', column: 'file_uploads', diff --git a/tests/core/internal/frame-elements.test.js b/packages/skyflow-js/tests/core/internal/frame-elements.test.js similarity index 98% rename from tests/core/internal/frame-elements.test.js rename to packages/skyflow-js/tests/core/internal/frame-elements.test.js index f85bf2bda..2dbc03ad2 100644 --- a/tests/core/internal/frame-elements.test.js +++ b/packages/skyflow-js/tests/core/internal/frame-elements.test.js @@ -2,11 +2,11 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import FrameElementInit from './../../../src/core/internal/frame-element-init'; +import FrameElementInit from './../../../src/internal/frame-element-init'; import { FRAME_ELEMENT, ELEMENT_EVENTS_TO_IFRAME -} from '../../../src/core/constants'; +} from '@core/constants'; const stylesOptions = { inputStyles: { diff --git a/tests/core/internal/iframe-form/iframe-form.test.js b/packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js similarity index 97% rename from tests/core/internal/iframe-form/iframe-form.test.js rename to packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js index ed7f65df7..0d7adcf55 100644 --- a/tests/core/internal/iframe-form/iframe-form.test.js +++ b/packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js @@ -2,16 +2,16 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import { COLLECT_FRAME_CONTROLLER, ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, ElementType, FRAME_ELEMENT } from '../../../../src/core/constants'; +import { COLLECT_FRAME_CONTROLLER, ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, BaseElementType, FRAME_ELEMENT } from '@core/constants'; import { Env, LogLevel, ValidationRuleType } from '../../../../src/utils/common'; -import IFrameFormElement from '../../../../src/core/internal/iframe-form' -import * as busEvents from '../../../../src/utils/bus-events'; -import SkyflowError from '../../../../src/libs/skyflow-error'; -import logs from '../../../../src/utils/logs'; +import IFrameFormElement from '@core/internal/iframe-form' +import * as busEvents from '@core/utils/bus-events'; +import SkyflowError from '@core/errors'; +import logs from '@core/utils/logs'; import { ContainerType } from '../../../../src/skyflow'; -import { formatOptions } from '../../../../src/libs/element-options'; +import { formatOptions } from '@core/libs/element-options'; import { parameterizedString } from '../../../../src/utils/logs-helper'; -import FrameElementInit from '../../../../src/core/internal/frame-element-init'; +import FrameElementInit from '../../../../src/internal/frame-element-init'; const tableCol = btoa('1234') @@ -135,50 +135,50 @@ describe('test iframeFormelement', () => { const elementsList = [ { element: test_collect_element, - type: ElementType.CARD_NUMBER, + type: BaseElementType.CARD_NUMBER, input: '4111111111111111', expected:'41111111XXXXXXXX' }, { element: collect_element, - type: ElementType.CVV, + type: BaseElementType.CVV, input: '1234', expected: undefined }, { element: test_collect_element, - type: ElementType.CARDHOLDER_NAME, + type: BaseElementType.CARDHOLDER_NAME, input: 'john doe', expected: undefined }, { element: test_collect_element, - type: ElementType.EXPIRATION_DATE, + type: BaseElementType.EXPIRATION_DATE, input: '12/30', format: 'MM/YY', expected: undefined, }, { element: test_collect_element, - type: ElementType.EXPIRATION_MONTH, + type: BaseElementType.EXPIRATION_MONTH, input: '11', expected: undefined }, { element: test_collect_element, - type: ElementType.EXPIRATION_YEAR, + type: BaseElementType.EXPIRATION_YEAR, input: '29', expected: undefined }, { element: test_collect_element, - type: ElementType.PIN, + type: BaseElementType.PIN, input: '2912', expected: undefined }, { element: test_collect_element, - type: ElementType.INPUT_FIELD, + type: BaseElementType.INPUT_FIELD, input: '212-61-2465', expected: undefined }, @@ -202,50 +202,50 @@ describe('test iframeFormelement', () => { const elementsList = [ { element: test_collect_element, - type: ElementType.CARD_NUMBER, + type: BaseElementType.CARD_NUMBER, input: '4111111111111111', expected:'4111111111111111' }, { element: collect_element, - type: ElementType.CVV, + type: BaseElementType.CVV, input: '1234', expected: '1234' }, { element: test_collect_element, - type: ElementType.CARDHOLDER_NAME, + type: BaseElementType.CARDHOLDER_NAME, input: 'john doe', expected: 'john doe' }, { element: test_collect_element, - type: ElementType.EXPIRATION_DATE, + type: BaseElementType.EXPIRATION_DATE, input: '12/30', format: 'MM/YY', expected: '12/30', }, { element: test_collect_element, - type: ElementType.EXPIRATION_MONTH, + type: BaseElementType.EXPIRATION_MONTH, input: '11', expected: '11' }, { element: test_collect_element, - type: ElementType.EXPIRATION_YEAR, + type: BaseElementType.EXPIRATION_YEAR, input: '29', expected: '29' }, { element: test_collect_element, - type: ElementType.PIN, + type: BaseElementType.PIN, input: '2912', expected: '2912' }, { element: test_collect_element, - type: ElementType.INPUT_FIELD, + type: BaseElementType.INPUT_FIELD, input: '212-61-2465', expected: '212-61-2465' }, diff --git a/tests/core/internal/internal-index.test.js b/packages/skyflow-js/tests/core/internal/internal-index.test.js similarity index 99% rename from tests/core/internal/internal-index.test.js rename to packages/skyflow-js/tests/core/internal/internal-index.test.js index 0b3a0fdc8..484af58d6 100644 --- a/tests/core/internal/internal-index.test.js +++ b/packages/skyflow-js/tests/core/internal/internal-index.test.js @@ -1,10 +1,10 @@ import bus from 'framebus'; -import FrameElement from '../../../src/core/internal/index'; -import * as validators from '../../../src/utils/validators'; -import * as helpers from '../../../src/utils/helpers'; -import { getMaskedOutput, domReady } from '../../../src/utils/helpers'; -import { COLLECT_FRAME_CONTROLLER, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, CARD_ENCODED_ICONS, INPUT_KEYBOARD_EVENTS, ELEMENT_EVENTS_TO_CLIENT, ElementType, STYLE_TYPE } from '../../../src/core/constants'; -import IFrameFormElement from '../../../src/core/internal/iframe-form'; +import FrameElement from '@core/internal'; +import * as validators from '@core/validators'; +import * as helpers from '@core/helpers'; +import { getMaskedOutput, domReady } from '@core/helpers'; +import { COLLECT_FRAME_CONTROLLER, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, CARD_ENCODED_ICONS, INPUT_KEYBOARD_EVENTS, ELEMENT_EVENTS_TO_CLIENT, BaseElementType, STYLE_TYPE } from '@core/constants'; +import IFrameFormElement from '@core/internal/iframe-form'; import { ValidationRuleType } from '../../../src/utils/common'; import { get } from 'lodash'; @@ -1332,7 +1332,7 @@ describe('FrameElement', () => { it('should update input styles when enablecardicon are provided', () => { const mockOptions = { enableCardIcon: true, - elementType: ElementType.CARD_NUMBER + elementType: BaseElementType.CARD_NUMBER }; frameElement.updateOptions(mockOptions); diff --git a/tests/core/internal/reveal/reveal-frame.test.js b/packages/skyflow-js/tests/core/internal/reveal/reveal-frame.test.js similarity index 99% rename from tests/core/internal/reveal/reveal-frame.test.js rename to packages/skyflow-js/tests/core/internal/reveal/reveal-frame.test.js index a614eb15e..4f99ad762 100644 --- a/tests/core/internal/reveal/reveal-frame.test.js +++ b/packages/skyflow-js/tests/core/internal/reveal/reveal-frame.test.js @@ -2,18 +2,18 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from "framebus"; -import RevealFrame from "../../../../src/core/internal/reveal/reveal-frame"; -import { DEFAULT_FILE_RENDER_ERROR, ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_ERROR_TEXT, REVEAL_ELEMENT_OPTIONS_TYPES, REVEAL_TYPES } from "../../../../src/core/constants"; +import RevealFrame from "../../../../src/internal/reveal/reveal-frame"; +import { DEFAULT_FILE_RENDER_ERROR, ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_ERROR_TEXT, REVEAL_ELEMENT_OPTIONS_TYPES, REVEAL_TYPES } from "@core/constants"; import { Env, LogLevel, RedactionType } from "../../../../src/utils/common"; -import getCssClassesFromJss from "../../../../src/libs/jss-styles"; -import { getFileURLFromVaultBySkyflowIDComposable } from "../../../../src/core-utils/reveal"; -import properties from "../../../../src/properties"; +import getCssClassesFromJss from "@core/libs/jss-styles"; +import { getFileURLFromVaultBySkyflowIDComposable } from "../../../../src/api-utils/reveal"; +import properties from "@core/properties"; // / mock the getFileURLFromVaultBySkyflowIDComposable function and keep original other things // Dynamic mock for getFileURLFromVaultBySkyflowIDComposable allowing per-test resolve/reject setup const mockGetFileURLFromVaultBySkyflowIDComposable = jest.fn(); -jest.mock('../../../../src/core-utils/reveal', () => { - const original = jest.requireActual('../../../../src/core-utils/reveal'); +jest.mock('../../../../src/api-utils/reveal', () => { + const original = jest.requireActual('../../../../src/api-utils/reveal'); return { ...original, getFileURLFromVaultBySkyflowIDComposable: (...args) => mockGetFileURLFromVaultBySkyflowIDComposable(...args), diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js similarity index 98% rename from tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js rename to packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js index e579fca43..a97641dd6 100644 --- a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js +++ b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js @@ -2,16 +2,16 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import { COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME } from '../../../../src/core/constants'; -import clientModule from '../../../../src/client'; -import * as busEvents from '../../../../src/utils/bus-events'; +import { COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME } from '@core/constants'; +import clientModule from '@core/client'; +import * as busEvents from '@core/utils/bus-events'; import { LogLevel, Env } from '../../../../src/utils/common'; -import SkyflowFrameController from '../../../../src/core/internal/skyflow-frame/skyflow-frame-controller'; -import SkyflowError from '../../../../src/libs/skyflow-error'; +import SkyflowFrameController from '../../../../src/internal/skyflow-frame/skyflow-frame-controller'; +import SkyflowError from '@core/errors'; busEvents.getAccessToken = jest.fn(() => Promise.resolve('access token')); const on = jest.fn(); const emit = jest.fn(); -jest.mock('../../../../src/libs/uuid', () => ({ +jest.mock('@core/libs/uuid', () => ({ __esModule: true, default: jest.fn(() => (mockUuid)), })); @@ -1150,10 +1150,10 @@ describe('SkyflowFrameController - tokenize function', () => { }, })); - jest.spyOn(require('../../../../src/core-utils/collect'), 'checkForElementMatchRule').mockReturnValue(true); - jest.spyOn(require('../../../../src/core-utils/collect'), 'checkForValueMatch').mockReturnValue(true); + jest.spyOn(require('@core/helpers'), 'checkForElementMatchRule').mockReturnValue(true); + jest.spyOn(require('@core/helpers'), 'checkForValueMatch').mockReturnValue(true); - jest.spyOn(require('../../../../src/core-utils/collect'), 'constructElementsInsertReq').mockImplementation(() => { + jest.spyOn(require('@core/api-utils/collect'), 'constructElementsInsertReq').mockImplementation(() => { return [ { records: [] }, { updateRecords: [{ table: 'testTable', fields: { key: 'value' }, skyflowID: '123' }] }, diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts similarity index 98% rename from tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts rename to packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts index 444674f7f..876484f16 100644 --- a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts +++ b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts @@ -5,17 +5,17 @@ import bus from "framebus"; import { COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME, -} from "../../../../src/core/constants"; -import clientModule from "../../../../src/client"; -import * as busEvents from "../../../../src/utils/bus-events"; +} from "@core/constants"; +import clientModule from "@core/client"; +import * as busEvents from "@core/utils/bus-events"; import { LogLevel, Env, InsertResponse } from "../../../../src/utils/common"; -import SkyflowFrameController from "../../../../src/core/internal/skyflow-frame/skyflow-frame-controller"; -import Client from "../../../../src/client"; +import SkyflowFrameController from "../../../../src/internal/skyflow-frame/skyflow-frame-controller"; +import Client from "@core/client"; import { ISkyflow } from "../../../../src/skyflow"; import { TokenizeDataInput, UploadFileDataInput, -} from "../../../../src/core/internal/internal-types"; +} from "../../../../src/internal/internal-types"; jest .spyOn(busEvents, "getAccessToken") @@ -24,7 +24,7 @@ jest const on = jest.fn(); const emit = jest.fn(); -jest.mock("../../../../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => mockUuid), })); @@ -1151,20 +1151,20 @@ describe("SkyflowFrameController - tokenize function", () => { jest .spyOn( - require("../../../../src/core-utils/collect"), + require("@core/helpers"), "checkForElementMatchRule" ) .mockReturnValue(true); jest .spyOn( - require("../../../../src/core-utils/collect"), + require("@core/helpers"), "checkForValueMatch" ) .mockReturnValue(true); jest .spyOn( - require("../../../../src/core-utils/collect"), + require("@core/api-utils/collect"), "constructElementsInsertReq" ) .mockImplementation(() => { diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js similarity index 98% rename from tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js rename to packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js index aae33fed7..030a5d481 100644 --- a/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js +++ b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js @@ -2,21 +2,21 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import { COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_TYPES, ElementType, PUREJS_TYPES, REVEAL_TYPES } from '../../../../src/core/constants'; -import clientModule from '../../../../src/client'; -import * as busEvents from '../../../../src/utils/bus-events'; +import { COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_TYPES, BaseElementType, PUREJS_TYPES, REVEAL_TYPES } from '@core/constants'; +import clientModule from '@core/client'; +import * as busEvents from '@core/utils/bus-events'; import { LogLevel, Env, RedactionType } from '../../../../src/utils/common'; -import SkyflowFrameController from '../../../../src/core/internal/skyflow-frame/skyflow-frame-controller'; -import RevealFrame from '../../../../src/core/internal/reveal/reveal-frame'; -import uuid from '../../../../src/libs/uuid'; -// import IFrame from '../../../../src/core/external/common/iframe'; -import CollectContainer from '../../../../src/core/external/collect/collect-container'; -// import CollectElement from '../../../../src/core/external/collect/collect-element'; +import SkyflowFrameController from '../../../../src/internal/skyflow-frame/skyflow-frame-controller'; +import RevealFrame from '../../../../src/internal/reveal/reveal-frame'; +import uuid from '@core/libs/uuid'; +// import IFrame from '@core/external/common/iframe'; +import CollectContainer from '../../../../src/external/collect/collect-container'; +// import CollectElement from '@core/external/collect/collect-element'; busEvents.getAccessToken = jest.fn(() => Promise.resolve('access token')); const on = jest.fn(); const emit = jest.fn(); -jest.mock('../../../../src/libs/uuid', () => ({ +jest.mock('@core/libs/uuid', () => ({ __esModule: true, default: jest.fn(() => (mockUuid)), })); diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts similarity index 99% rename from tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts rename to packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts index ca5598beb..3e51a5de0 100644 --- a/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts +++ b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts @@ -8,9 +8,9 @@ import { ELEMENT_EVENTS_TO_IFRAME, PUREJS_TYPES, REVEAL_TYPES, -} from "../../../../src/core/constants"; -import clientModule from "../../../../src/client"; -import * as busEvents from "../../../../src/utils/bus-events"; +} from "@core/constants"; +import clientModule from "@core/client"; +import * as busEvents from "@core/utils/bus-events"; import { LogLevel, Env, @@ -20,21 +20,21 @@ import { IGetOptions, IDeleteRecordInput, } from "../../../../src/utils/common"; -import SkyflowFrameController from "../../../../src/core/internal/skyflow-frame/skyflow-frame-controller"; +import SkyflowFrameController from "../../../../src/internal/skyflow-frame/skyflow-frame-controller"; import { ErrorType, InsertOptions } from "../../../../src/index-node"; import { ISkyflow } from "../../../../src/skyflow"; -import Client from "../../../../src/client"; +import Client from "@core/client"; import { set } from "core-js/core/dict"; -jest.mock("../../../../src/utils/bus-events", () => ({ - ...jest.requireActual("../../../../src/utils/bus-events"), +jest.mock("@core/utils/bus-events", () => ({ + ...jest.requireActual("@core/utils/bus-events"), getAccessToken: jest.fn(() => Promise.resolve("access token")), })); const on = jest.fn(); const emit = jest.fn(); -jest.mock("../../../../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => mockUuid), })); diff --git a/tests/event-emitter/emitter.test.js b/packages/skyflow-js/tests/event-emitter/emitter.test.js similarity index 96% rename from tests/event-emitter/emitter.test.js rename to packages/skyflow-js/tests/event-emitter/emitter.test.js index bc54bd306..26881e213 100644 --- a/tests/event-emitter/emitter.test.js +++ b/packages/skyflow-js/tests/event-emitter/emitter.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import EventEmitter from '../../src/event-emitter/index'; +import EventEmitter from '@core/event-emitter'; describe('Event emitter test', () => { const eventObj = new EventEmitter(); diff --git a/tests/index-internal.test.js b/packages/skyflow-js/tests/index-internal.test.js similarity index 82% rename from tests/index-internal.test.js rename to packages/skyflow-js/tests/index-internal.test.js index a4aa7aed4..f771226d4 100644 --- a/tests/index-internal.test.js +++ b/packages/skyflow-js/tests/index-internal.test.js @@ -1,16 +1,16 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { FrameController } from './../src/core/internal'; -import FrameElementInit from './../src/core/internal/frame-element-init'; -import RevealFrame from './../src/core/internal/reveal/reveal-frame'; -import SkyflowFrameController from './../src/core/internal/skyflow-frame/skyflow-frame-controller'; +import { FrameController } from '@core/internal'; +import FrameElementInit from './../src/internal/frame-element-init'; +import RevealFrame from './../src/internal/reveal/reveal-frame'; +import SkyflowFrameController from './../src/internal/skyflow-frame/skyflow-frame-controller'; import { COMPOSABLE_REVEAL, FRAME_ELEMENT, FRAME_REVEAL, SKYFLOW_FRAME_CONTROLLER, - } from './../src/core/constants'; + } from '@core/constants'; jest.mock('framebus') jest.mock('jquery-mask-plugin/dist/jquery.mask.min') @@ -32,7 +32,7 @@ describe('test index-internal', () => { })); const mock = jest.fn(); - jest.mock('./../src/core/internal/skyflow-frame/skyflow-frame-controller', () => ({ + jest.mock('./../src/internal/skyflow-frame/skyflow-frame-controller', () => ({ init: mock, })); @@ -51,7 +51,7 @@ describe('test index-internal', () => { })); const mock = jest.fn() - jest.mock( './../src/core/internal/frame-element-init', () => { + jest.mock( './../src/internal/frame-element-init', () => { return { startFrameElement: mock } @@ -73,7 +73,7 @@ describe('test index-internal', () => { })); const mock = jest.fn() - jest.mock( './../src/core/internal/frame-element-init.ts', () => { + jest.mock( './../src/internal/frame-element-init.ts', () => { return { start: mock } @@ -95,7 +95,7 @@ describe('test index-internal', () => { })); const mock = jest.fn(); - jest.mock('./../src/core/internal/reveal/reveal-frame', () => { + jest.mock('./../src/internal/reveal/reveal-frame', () => { return { init: mock, }}); @@ -115,7 +115,7 @@ describe('test index-internal', () => { })); const mock = jest.fn(); - jest.mock('./../src/core/internal/reveal/reveal-frame', () => { + jest.mock('./../src/internal/reveal/reveal-frame', () => { return { init: mock, }}); @@ -136,7 +136,7 @@ describe('test index-internal', () => { const mock = jest.fn(); // For composable reveal frames, index-internal calls static startFrameElement() // on the default export class. Mock the module with a class exposing that static. - jest.mock('./../src/core/internal/composable-frame-element-init.ts', () => ({ + jest.mock('../src/internal/composable-frame-element-init', () => ({ __esModule: true, default: class MockComposableRevealInit { static startFrameElement() { mock(); } diff --git a/packages/skyflow-js/tests/jest.setup.js b/packages/skyflow-js/tests/jest.setup.js new file mode 100644 index 000000000..a8e6e6419 --- /dev/null +++ b/packages/skyflow-js/tests/jest.setup.js @@ -0,0 +1,10 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +// Mirror the webpack DefinePlugin SDK identity injection for the test runtime. +// jest does not run webpack, so define SDK_NAME/SDK_VERSION as globals from +// this package's own package.json (keeps telemetry output identical in tests). +const pkg = require('../package.json'); + +global.SDK_NAME = pkg.name; +global.SDK_VERSION = pkg.version; diff --git a/tests/libs/bus.test.js b/packages/skyflow-js/tests/libs/bus.test.js similarity index 96% rename from tests/libs/bus.test.js rename to packages/skyflow-js/tests/libs/bus.test.js index e574ac3b8..67e6f2c87 100644 --- a/tests/libs/bus.test.js +++ b/packages/skyflow-js/tests/libs/bus.test.js @@ -2,7 +2,7 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import Bus from '../../src/libs/bus'; +import Bus from '@core/libs/bus'; describe('bus test', () => { const busObj = new Bus(); diff --git a/tests/libs/element-options.test.js b/packages/skyflow-js/tests/libs/element-options.test.js similarity index 70% rename from tests/libs/element-options.test.js rename to packages/skyflow-js/tests/libs/element-options.test.js index f1673fd26..5e7c1e6d4 100644 --- a/tests/libs/element-options.test.js +++ b/packages/skyflow-js/tests/libs/element-options.test.js @@ -1,15 +1,15 @@ -import { CARDNUMBER_INPUT_FORMAT, CardType, ElementType } from "../../src/core/constants"; -import { formatOptions, formatValidations } from "../../src/libs/element-options"; +import { CARDNUMBER_INPUT_FORMAT, CardType, BaseElementType, FileElementType } from "@core/constants"; +import { formatOptions, formatValidations } from "@core/libs/element-options"; import { LogLevel } from "../../src/utils/common"; -import SKYFLOW_ERROR_CODE from "../../src/utils/constants"; +import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { parameterizedString } from "../../src/utils/logs-helper"; -import logs from "../../src/utils/logs"; -import { validateInputFormatOptions } from "../../src/utils/validators"; -import { DEFAULT_CARD_NUMBER_SEPERATOR } from "../../src/core/constants"; -import ComposableElement from "../../src/core/external/collect/compose-collect-element"; +import logs from "@core/utils/logs"; +import { validateInputFormatOptions } from "@core/validators"; +import { DEFAULT_CARD_NUMBER_SEPERATOR } from "@core/constants"; +import ComposableElement from "../../src/external/collect/compose-collect-element"; -jest.mock('../../src/utils/validators',()=>{ - const originalModule = jest.requireActual('../../src/utils/validators') +jest.mock('@core/validators',()=>{ + const originalModule = jest.requireActual('@core/validators') return { ...originalModule, validateInputFormatOptions : jest.fn(), @@ -19,98 +19,98 @@ jest.mock('../../src/utils/validators',()=>{ describe('test formatOptions function with format and translation', () => { test("formatOptions function should return existing options as is", () => { const options = { enableCardIcon: true } - expect(formatOptions(ElementType.CVV, options, LogLevel.ERROR)).toEqual({ ...options, required: false }) + expect(formatOptions(BaseElementType.CVV, options, LogLevel.ERROR)).toEqual({ ...options, required: false }) }); test('should throw warning if the format or translation is provided for not supported element types', () => { const spy = jest.spyOn(console, 'warn'); const options = { format: 'XXXX' } - formatOptions(ElementType.CVV, options, LogLevel.WARN); + formatOptions(BaseElementType.CVV, options, LogLevel.WARN); expect(spy).toBeCalledWith(`WARN: [Skyflow] ${parameterizedString(logs.warnLogs.INPUT_FORMATTING_NOT_SUPPROTED, - ElementType.CVV)}`); + BaseElementType.CVV)}`); expect(spy).toBeCalledTimes(1); - formatOptions(ElementType.EXPIRATION_MONTH, options, LogLevel.WARN); + formatOptions(BaseElementType.EXPIRATION_MONTH, options, LogLevel.WARN); expect(spy).toBeCalledWith(`WARN: [Skyflow] ${parameterizedString(logs.warnLogs.INPUT_FORMATTING_NOT_SUPPROTED, - ElementType.EXPIRATION_MONTH)}`); + BaseElementType.EXPIRATION_MONTH)}`); expect(spy).toBeCalledTimes(2); - formatOptions(ElementType.PIN, {enableCardIcon:true}, LogLevel.WARN); + formatOptions(BaseElementType.PIN, {enableCardIcon:true}, LogLevel.WARN); expect(spy).toBeCalledTimes(2); - formatOptions(ElementType.CARDHOLDER_NAME, options, LogLevel.WARN); + formatOptions(BaseElementType.CARDHOLDER_NAME, options, LogLevel.WARN); expect(spy).toBeCalledWith(`WARN: [Skyflow] ${parameterizedString(logs.warnLogs.INPUT_FORMATTING_NOT_SUPPROTED, - ElementType.CARDHOLDER_NAME)}`); + BaseElementType.CARDHOLDER_NAME)}`); expect(spy).toBeCalledTimes(3); - formatOptions(ElementType.FILE_INPUT, options, LogLevel.WARN); + formatOptions(FileElementType.FILE_INPUT, options, LogLevel.WARN); expect(spy).toBeCalledWith(`WARN: [Skyflow] ${parameterizedString(logs.warnLogs.INPUT_FORMATTING_NOT_SUPPROTED, - ElementType.FILE_INPUT)}`); + FileElementType.FILE_INPUT)}`); expect(spy).toBeCalledTimes(4); - formatOptions(ElementType.PIN, options, LogLevel.WARN); + formatOptions(BaseElementType.PIN, options, LogLevel.WARN); expect(spy).toBeCalledWith(`WARN: [Skyflow] ${parameterizedString(logs.warnLogs.INPUT_FORMATTING_NOT_SUPPROTED, - ElementType.PIN)}`); + BaseElementType.PIN)}`); expect(spy).toBeCalledTimes(5); }); test('should call validateInputFormatOptions function if the format or translation is provided for supported element types',()=>{ const options = {format:'XXXX',translation:{X:'[0-9]'}} - formatOptions(ElementType.INPUT_FIELD,options,LogLevel.ERROR); + formatOptions(BaseElementType.INPUT_FIELD,options,LogLevel.ERROR); expect(validateInputFormatOptions).toBeCalled(); }); test('should return mask array object with valid format and translation for input field type',()=>{ const options = {format:'XXXX',translation:{X:'[]'}} - const formattedOptions = formatOptions(ElementType.INPUT_FIELD,options,LogLevel.ERROR); + const formattedOptions = formatOptions(BaseElementType.INPUT_FIELD,options,LogLevel.ERROR); const res = { mask:[options.format,options.translation],required:false}; expect(formattedOptions).toEqual(res) }); test('should return mask array object with valid format and default translation for input field type',()=>{ const options = {format:'XXXX'} - const formattedOptions = formatOptions(ElementType.INPUT_FIELD,options,LogLevel.ERROR); + const formattedOptions = formatOptions(BaseElementType.INPUT_FIELD,options,LogLevel.ERROR); const res = { mask:[options.format,{X:'[0-9]'}],required:false}; expect(formattedOptions).toEqual(res) }); test('should return default cardSeperator in the options as default only for card number field type - no format',()=>{ - const formattedOptions = formatOptions(ElementType.CARD_NUMBER,{required:false},LogLevel.ERROR); + const formattedOptions = formatOptions(BaseElementType.CARD_NUMBER,{required:false},LogLevel.ERROR); expect(formattedOptions).toEqual({required:false,cardSeperator:DEFAULT_CARD_NUMBER_SEPERATOR,enableCardIcon:true}); }); test('should return default cardSeperator in the options as default only for card number field type - not allowed format',()=>{ - const formattedOptions = formatOptions(ElementType.CARD_NUMBER,{required:false,format:'XXXX/XXXX/XXXX/XXXX'},LogLevel.ERROR); + const formattedOptions = formatOptions(BaseElementType.CARD_NUMBER,{required:false,format:'XXXX/XXXX/XXXX/XXXX'},LogLevel.ERROR); expect(formattedOptions).toEqual({required:false,cardSeperator:DEFAULT_CARD_NUMBER_SEPERATOR,enableCardIcon:true}); }); test('should return hypen cardSeperator in the options format only for card number field type - with dash format',()=>{ const cardFormat = CARDNUMBER_INPUT_FORMAT.DASH_FORMAT - const formattedOptions = formatOptions(ElementType.CARD_NUMBER,{required:false,format:cardFormat},LogLevel.ERROR); + const formattedOptions = formatOptions(BaseElementType.CARD_NUMBER,{required:false,format:cardFormat},LogLevel.ERROR); expect(formattedOptions).toEqual({required:false,cardSeperator:'-',enableCardIcon:true}); }); test('should return space cardSeperator in the options format only for card number field type - space format',()=>{ const cardFormat = CARDNUMBER_INPUT_FORMAT.SPACE_FORMAT - const formattedOptions = formatOptions(ElementType.CARD_NUMBER,{required:false,format:cardFormat},LogLevel.ERROR); + const formattedOptions = formatOptions(BaseElementType.CARD_NUMBER,{required:false,format:cardFormat},LogLevel.ERROR); expect(formattedOptions).toEqual({required:false,cardSeperator:DEFAULT_CARD_NUMBER_SEPERATOR,enableCardIcon:true}); }); test('should return preserveFileName true when not provied in options',()=>{ - const formattedOptions = formatOptions(ElementType.FILE_INPUT,{required:true},LogLevel.ERROR); + const formattedOptions = formatOptions(FileElementType.FILE_INPUT,{required:true},LogLevel.ERROR); expect(formattedOptions).toEqual({required:true,preserveFileName:true}); }); test('should return preserveFileName false when not provied as false in options',()=>{ - const formattedOptions = formatOptions(ElementType.FILE_INPUT,{required:true,preserveFileName:false},LogLevel.ERROR); + const formattedOptions = formatOptions(FileElementType.FILE_INPUT,{required:true,preserveFileName:false},LogLevel.ERROR); expect(formattedOptions).toEqual({required:true,preserveFileName:false}); }); test('should throw errror for preserveFileName provied as not of boolean type',(done)=>{ try{ - formatOptions(ElementType.FILE_INPUT,{required:true,preserveFileName:undefined},LogLevel.ERROR); + formatOptions(FileElementType.FILE_INPUT,{required:true,preserveFileName:undefined},LogLevel.ERROR); done('should throw error'); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS.description, 'preserveFileName')) @@ -120,7 +120,7 @@ describe('test formatOptions function with format and translation', () => { test('should return masking in format options when masking is true',(done)=>{ try{ - formatOptions(ElementType.CARD_NUMBER,{masking: true},LogLevel.ERROR); + formatOptions(BaseElementType.CARD_NUMBER,{masking: true},LogLevel.ERROR); done(); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS.description, 'preserveFileName')) @@ -130,7 +130,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw error when masking not of boolean type',(done)=>{ try{ - formatOptions(ElementType.CARD_NUMBER,{required:true,masking: 'test'},LogLevel.ERROR); + formatOptions(BaseElementType.CARD_NUMBER,{required:true,masking: 'test'},LogLevel.ERROR); done(); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS.description, ['masking'], true)) @@ -140,7 +140,7 @@ describe('test formatOptions function with format and translation', () => { test('should return masking and maskingChar in format options when masking is true',(done)=>{ try{ - formatOptions(ElementType.CARD_NUMBER,{required:true,masking: true, maskingChar: '*'},LogLevel.ERROR); + formatOptions(BaseElementType.CARD_NUMBER,{required:true,masking: true, maskingChar: '*'},LogLevel.ERROR); done(); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS.description, 'preserveFileName')) @@ -150,7 +150,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw error when maskingChar is of length one',(done)=>{ try{ - formatOptions(ElementType.CVV,{required:true,masking: true, maskingChar:'**'},LogLevel.ERROR); + formatOptions(BaseElementType.CVV,{required:true,masking: true, maskingChar:'**'},LogLevel.ERROR); done(); }catch(err){ expect(err?.error?.description).toEqual(SKYFLOW_ERROR_CODE.INVALID_MASKING_CHARACTER.description, [], true) @@ -160,7 +160,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw errror for cardMetadata provied as not of object type',(done)=>{ try{ - formatOptions(ElementType.CARD_NUMBER,{cardMetadata:true},LogLevel.ERROR); + formatOptions(BaseElementType.CARD_NUMBER,{cardMetadata:true},LogLevel.ERROR); done('should throw error'); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_OPTION_CARD_METADATA.description)); @@ -170,7 +170,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw errror for cardMetadata provied value is object type',(done)=>{ try{ - formatOptions(ElementType.CARD_NUMBER,{cardMetadata:[]},LogLevel.ERROR); + formatOptions(BaseElementType.CARD_NUMBER,{cardMetadata:[]},LogLevel.ERROR); done('should throw error'); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_OPTION_CARD_METADATA.description)); @@ -180,7 +180,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw errror for cardMetadata schema provied value is array type',(done)=>{ try{ - formatOptions(ElementType.CARD_NUMBER,{cardMetadata:{scheme:{}}},LogLevel.ERROR); + formatOptions(BaseElementType.CARD_NUMBER,{cardMetadata:{scheme:{}}},LogLevel.ERROR); done('should throw error'); }catch(err){ expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_OPTION_CARD_SCHEME.description)); @@ -189,18 +189,18 @@ describe('test formatOptions function with format and translation', () => { }); test('should return the array of Cardtype provided in scheme of cardmetadata',()=>{ - const options = formatOptions(ElementType.CARD_NUMBER,{cardMetadata:{scheme:[CardType.VISA,CardType.CARTES_BANCAIRES]}},LogLevel.ERROR); + const options = formatOptions(BaseElementType.CARD_NUMBER,{cardMetadata:{scheme:[CardType.VISA,CardType.CARTES_BANCAIRES]}},LogLevel.ERROR); expect(options).toEqual({cardMetadata:{scheme:[CardType.VISA,CardType.CARTES_BANCAIRES]}, "cardSeperator": " ","enableCardIcon": true,"required": false,}) }); test('should include maxFileSize in formatted options for MULTI_FILE_INPUT', () => { - const formattedOptions = formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileSize: 4000000 }, LogLevel.ERROR); + const formattedOptions = formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileSize: 4000000 }, LogLevel.ERROR); expect(formattedOptions.maxFileSize).toBe(4000000); }); test('should throw error for maxFileSize provided as non-number for MULTI_FILE_INPUT', (done) => { try { - formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileSize: 'large' }, LogLevel.ERROR); + formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileSize: 'large' }, LogLevel.ERROR); done('should throw error'); } catch (err) { expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_POSITIVE_NUMBER_OPTIONS.description, 'maxFileSize')); @@ -210,7 +210,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw error for maxFileSize provided as zero for MULTI_FILE_INPUT', (done) => { try { - formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileSize: 0 }, LogLevel.ERROR); + formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileSize: 0 }, LogLevel.ERROR); done('should throw error'); } catch (err) { expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_POSITIVE_NUMBER_OPTIONS.description, 'maxFileSize')); @@ -220,7 +220,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw error for maxFileSize provided as negative number for MULTI_FILE_INPUT', (done) => { try { - formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileSize: -1000 }, LogLevel.ERROR); + formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileSize: -1000 }, LogLevel.ERROR); done('should throw error'); } catch (err) { expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_POSITIVE_NUMBER_OPTIONS.description, 'maxFileSize')); @@ -229,13 +229,13 @@ describe('test formatOptions function with format and translation', () => { }); test('should include maxFileCount in formatted options for MULTI_FILE_INPUT', () => { - const formattedOptions = formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileCount: 2 }, LogLevel.ERROR); + const formattedOptions = formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileCount: 2 }, LogLevel.ERROR); expect(formattedOptions.maxFileCount).toBe(2); }); test('should throw error for maxFileCount provided as non-integer for MULTI_FILE_INPUT', (done) => { try { - formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileCount: 2.5 }, LogLevel.ERROR); + formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileCount: 2.5 }, LogLevel.ERROR); done('should throw error'); } catch (err) { expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_POSITIVE_NUMBER_OPTIONS.description, 'maxFileCount')); @@ -245,7 +245,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw error for maxFileCount provided as zero for MULTI_FILE_INPUT', (done) => { try { - formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileCount: 0 }, LogLevel.ERROR); + formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileCount: 0 }, LogLevel.ERROR); done('should throw error'); } catch (err) { expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_POSITIVE_NUMBER_OPTIONS.description, 'maxFileCount')); @@ -255,7 +255,7 @@ describe('test formatOptions function with format and translation', () => { test('should throw error for maxFileCount provided as negative number for MULTI_FILE_INPUT', (done) => { try { - formatOptions(ElementType.MULTI_FILE_INPUT, { maxFileCount: -1 }, LogLevel.ERROR); + formatOptions(FileElementType.MULTI_FILE_INPUT, { maxFileCount: -1 }, LogLevel.ERROR); done('should throw error'); } catch (err) { expect(err?.error?.description).toEqual(parameterizedString(SKYFLOW_ERROR_CODE.INVALID_POSITIVE_NUMBER_OPTIONS.description, 'maxFileCount')); @@ -264,12 +264,12 @@ describe('test formatOptions function with format and translation', () => { }); test('should not include maxFileSize in formatted options for FILE_INPUT', () => { - const formattedOptions = formatOptions(ElementType.FILE_INPUT, { maxFileSize: 4000000 }, LogLevel.ERROR); + const formattedOptions = formatOptions(FileElementType.FILE_INPUT, { maxFileSize: 4000000 }, LogLevel.ERROR); expect(formattedOptions.maxFileSize).toBeUndefined(); }); test('should not include maxFileCount in formatted options for FILE_INPUT', () => { - const formattedOptions = formatOptions(ElementType.FILE_INPUT, { maxFileCount: 2 }, LogLevel.ERROR); + const formattedOptions = formatOptions(FileElementType.FILE_INPUT, { maxFileCount: 2 }, LogLevel.ERROR); expect(formattedOptions.maxFileCount).toBeUndefined(); }); diff --git a/tests/libs/regex.test.js b/packages/skyflow-js/tests/libs/regex.test.js similarity index 79% rename from tests/libs/regex.test.js rename to packages/skyflow-js/tests/libs/regex.test.js index 7c6a3ad08..ea5435d95 100644 --- a/tests/libs/regex.test.js +++ b/packages/skyflow-js/tests/libs/regex.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import regExFromString from '../../src/libs/regex'; +import regExFromString from '@core/libs/regex'; describe('construct regex', () => { it('construct regex', () => { diff --git a/tests/libs/styles.test.js b/packages/skyflow-js/tests/libs/styles.test.js similarity index 96% rename from tests/libs/styles.test.js rename to packages/skyflow-js/tests/libs/styles.test.js index e3f8bb7b0..25ed65de2 100644 --- a/tests/libs/styles.test.js +++ b/packages/skyflow-js/tests/libs/styles.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { buildStylesFromClassesAndStyles, getFlexGridStyles } from '../../src/libs/styles'; +import { buildStylesFromClassesAndStyles, getFlexGridStyles } from '@core/libs/styles'; const styles = { base: { diff --git a/tests/metrics/metrics.test.js b/packages/skyflow-js/tests/metrics/metrics.test.js similarity index 99% rename from tests/metrics/metrics.test.js rename to packages/skyflow-js/tests/metrics/metrics.test.js index aa5a23b54..3c748ff56 100644 --- a/tests/metrics/metrics.test.js +++ b/packages/skyflow-js/tests/metrics/metrics.test.js @@ -5,7 +5,7 @@ import { initalizeMetricObject, getEventStatus, METRIC_OBJECT, -} from '../../src/metrics/index'; +} from '@core/metrics'; describe('metric object test', () => { describe('METRIC_OBJECT', () => { diff --git a/tests/skyflow-bearer-token.test.js b/packages/skyflow-js/tests/skyflow-bearer-token.test.js similarity index 94% rename from tests/skyflow-bearer-token.test.js rename to packages/skyflow-js/tests/skyflow-bearer-token.test.js index df16d7cb3..007c227f0 100644 --- a/tests/skyflow-bearer-token.test.js +++ b/packages/skyflow-js/tests/skyflow-bearer-token.test.js @@ -1,15 +1,15 @@ import Skyflow, { ContainerType, LogLevel } from '../src/skyflow'; -import isTokenValid from '../src/utils/jwt-utils'; +import isTokenValid from '@core/utils/jwt-utils'; // Mock uuid to keep deterministic -jest.mock('../src/libs/uuid', () => ({ +jest.mock('@core/libs/uuid', () => ({ __esModule: true, default: jest.fn(() => 'test-uuid'), })); // Mock CollectContainer to capture constructor argument let capturedConfig; // will hold the config object passed to container -jest.mock('../src/core/external/collect/collect-container', () => ({ +jest.mock('../src/external/collect/collect-container', () => ({ __esModule: true, default: jest.fn().mockImplementation((config) => { capturedConfig = config; // grab config which has getSkyflowBearerToken @@ -19,7 +19,7 @@ jest.mock('../src/core/external/collect/collect-container', () => ({ // Keep reveal/composable containers real (not used here) -jest.mock('../src/utils/jwt-utils', () => ({ +jest.mock('@core/utils/jwt-utils', () => ({ __esModule: true, default: jest.fn(() => true), // override per-test for validity scenarios })); diff --git a/tests/skyflow.test.js b/packages/skyflow-js/tests/skyflow.test.js similarity index 98% rename from tests/skyflow.test.js rename to packages/skyflow-js/tests/skyflow.test.js index bcb208376..dbf466fa1 100644 --- a/tests/skyflow.test.js +++ b/packages/skyflow-js/tests/skyflow.test.js @@ -3,22 +3,22 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; import Skyflow, { ContainerType } from '../src/skyflow'; -import CollectContainer from '../src/core/external/collect/collect-container'; -import RevealContainer from '../src/core/external/reveal/reveal-container'; -import * as iframerUtils from '../src/iframe-libs/iframer'; -import { ElementType, ELEMENT_EVENTS_TO_IFRAME } from '../src/core/constants'; +import CollectContainer from '../src/external/collect/collect-container'; +import RevealContainer from '../src/external/reveal/reveal-container'; +import * as iframerUtils from '@core/iframe-libs/iframer'; +import { BaseElementType, ELEMENT_EVENTS_TO_IFRAME } from '@core/constants'; import { Env, EventName, LogLevel, RedactionType, RequestMethod, ValidationRuleType } from '../src/utils/common'; -import ComposableContainer from '../src/core/external/collect/compose-collect-container'; -import SkyflowContainer from '../src/core/external/skyflow-container'; -import Client from '../src/client' -import logs from '../src/utils/logs'; +import ComposableContainer from '../src/external/collect/compose-collect-container'; +import SkyflowContainer from '../src/external/skyflow-container'; +import Client from '@core/client' +import logs from '@core/utils/logs'; import { ComposableRevealContainer } from '../src/index-node'; -jest.mock('../src/utils/jwt-utils', () => ({ +jest.mock('@core/utils/jwt-utils', () => ({ __esModule: true, default: jest.fn(() => true), })); -jest.mock('../src/libs/uuid', () => ({ +jest.mock('@core/libs/uuid', () => ({ __esModule: true, default: jest.fn(() => 'b5cbf425-6578-4d40-be88-82a748c36c60'), })); @@ -1580,10 +1580,10 @@ describe('Skyflow Enums', () => { }); test('Skyflow.ElementType', () => { - expect(Skyflow.ElementType.CARDHOLDER_NAME).toEqual(ElementType.CARDHOLDER_NAME); - expect(Skyflow.ElementType.CARD_NUMBER).toEqual(ElementType.CARD_NUMBER); - expect(Skyflow.ElementType.CVV).toEqual(ElementType.CVV); - expect(Skyflow.ElementType.EXPIRATION_DATE).toEqual(ElementType.EXPIRATION_DATE); + expect(Skyflow.ElementType.CARDHOLDER_NAME).toEqual(BaseElementType.CARDHOLDER_NAME); + expect(Skyflow.ElementType.CARD_NUMBER).toEqual(BaseElementType.CARD_NUMBER); + expect(Skyflow.ElementType.CVV).toEqual(BaseElementType.CVV); + expect(Skyflow.ElementType.EXPIRATION_DATE).toEqual(BaseElementType.EXPIRATION_DATE); }); test('Skyflow.RedactionType', () => { diff --git a/tests/skyflow.test.ts b/packages/skyflow-js/tests/skyflow.test.ts similarity index 99% rename from tests/skyflow.test.ts rename to packages/skyflow-js/tests/skyflow.test.ts index 7cb90c1e5..33d083c9a 100644 --- a/tests/skyflow.test.ts +++ b/packages/skyflow-js/tests/skyflow.test.ts @@ -3,8 +3,8 @@ Copyright (c) 2025 Skyflow, Inc. */ import bus from "framebus"; import Skyflow, { ISkyflow } from "../src/skyflow"; -import * as iframerUtils from "../src/iframe-libs/iframer"; -import { ELEMENT_EVENTS_TO_IFRAME } from "../src/core/constants"; +import * as iframerUtils from "@core/iframe-libs/iframer"; +import { ELEMENT_EVENTS_TO_IFRAME } from "@core/constants"; import { DeleteResponse, DeleteResponseRecord, @@ -35,11 +35,11 @@ import { UpdateResponse, } from "../src/utils/common"; -jest.mock("../src/utils/jwt-utils", () => ({ +jest.mock("@core/utils/jwt-utils", () => ({ __esModule: true, default: jest.fn(() => true), })); -jest.mock("../src/libs/uuid", () => ({ +jest.mock("@core/libs/uuid", () => ({ __esModule: true, default: jest.fn(() => "b5cbf425-6578-4d40-be88-82a748c36c60"), })); diff --git a/tests/utils/bus-events.test.js b/packages/skyflow-js/tests/utils/bus-events.test.js similarity index 65% rename from tests/utils/bus-events.test.js rename to packages/skyflow-js/tests/utils/bus-events.test.js index 70e495366..48999bb08 100644 --- a/tests/utils/bus-events.test.js +++ b/packages/skyflow-js/tests/utils/bus-events.test.js @@ -1,11 +1,11 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { getAccessToken } from "../../src/utils/bus-events"; -import { updateElementState } from "../../src/utils/bus-events"; +import { getAccessToken } from "@core/utils/bus-events"; +import { updateElementState } from "@core/utils/bus-events"; import bus from 'framebus'; -import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT } from "../../src/core/constants"; -import logs from "../../src/utils/logs"; +import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT } from "@core/constants"; +import logs from "@core/utils/logs"; const on = jest.fn(); const emit = jest.fn() describe("Utils/Bus Events",()=>{ @@ -49,35 +49,6 @@ describe("Utils/Bus Events",()=>{ done(); }); }); - - test("GetAccessToken Without parameter Fn valid token,",(done)=>{ - const response = getAccessToken(); - const emitEventName = emitSpy.mock.calls[1][0]; - const emitCb = emitSpy.mock.calls[1][2]; - expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN); - emitCb({authToken:"access_Token"}); - response.then((data)=>{ - expect(data).toEqual("access_Token"); - done(); - }).catch((err)=>{ - expect(err).toBeUndefined(); - done(); - }); - }); - test("GetAccessToken Without parameter Fn Invalid token",(done)=>{ - const response = getAccessToken(); - const emitEventName = emitSpy.mock.calls[1][0]; - const emitCb = emitSpy.mock.calls[1][2]; - expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN); - emitCb({error:"invalid_token"}); - response.then((data)=>{ - expect(data).toBeUndefined(); - done(); - }).catch((err)=>{ - expect(err).toEqual("invalid_token"); - done(); - }); - }); }); diff --git a/tests/utils/helpers.test.js b/packages/skyflow-js/tests/utils/helpers.test.js similarity index 88% rename from tests/utils/helpers.test.js rename to packages/skyflow-js/tests/utils/helpers.test.js index 0466d85db..b980fd69b 100644 --- a/tests/utils/helpers.test.js +++ b/packages/skyflow-js/tests/utils/helpers.test.js @@ -1,8 +1,8 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { CardType, ElementType,COPY_UTILS, CARD_NUMBER_MASK, DEFAULT_CARD_NUMBER_SEPERATOR, CARD_NUMBER_HYPEN_SEPERATOR } from '../../src/core/constants'; -import SKYFLOW_ERROR_CODE from '../../src/utils/constants'; +import { CardType, BaseElementType,COPY_UTILS, CARD_NUMBER_MASK, DEFAULT_CARD_NUMBER_SEPERATOR, CARD_NUMBER_HYPEN_SEPERATOR } from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import { replaceIdInResponseXml, appendZeroToOne, @@ -39,7 +39,7 @@ import { isValidURL } from '../../src/utils/validators/index'; const mockUUID = '1234' -jest.mock('../../src/libs/uuid',()=>({ +jest.mock('@core/libs/uuid',()=>({ __esModule: true, default:jest.fn(()=>(mockUUID)), })); @@ -47,48 +47,48 @@ jest.mock('../../src/libs/uuid',()=>({ describe('bin data for for all card number except AMEX element type on CHANGE event', () => { test("in PROD return bin data only for card number element", () => { expect(detectCardType("4111 1111 1111 1111")).toBe(CardType.VISA) - expect(getReturnValue("4111 1111 1111 1111", ElementType.CARD_NUMBER, false)).toBe("41111111XXXXXXXX") - expect(getReturnValue("4111 1111 ", ElementType.CARD_NUMBER, false)).toBe("41111111") + expect(getReturnValue("4111 1111 1111 1111", BaseElementType.CARD_NUMBER, false)).toBe("41111111XXXXXXXX") + expect(getReturnValue("4111 1111 ", BaseElementType.CARD_NUMBER, false)).toBe("41111111") expect(detectCardType("5105 1051 0510 5100")).toBe(CardType.MASTERCARD) - expect(getReturnValue("5105 1051 0510 5100", ElementType.CARD_NUMBER, false)).toBe("51051051XXXXXXXX") + expect(getReturnValue("5105 1051 0510 5100", BaseElementType.CARD_NUMBER, false)).toBe("51051051XXXXXXXX") expect(detectCardType("5066 9911 1111 1118")).toBe(CardType.DEFAULT) - expect(getReturnValue("5066 9911 1111 1118", ElementType.CARD_NUMBER, false)).toBe("50669911XXXXXXXX") - expect(getReturnValue("123", ElementType.CVV, false)).toBe(undefined) - expect(getReturnValue("name", ElementType.CARDHOLDER_NAME, false)).toBe(undefined) - expect(getReturnValue("02", ElementType.EXPIRATION_MONTH, false)).toBe(undefined) - expect(getReturnValue("2025", ElementType.EXPIRATION_YEAR, false)).toBe(undefined) - expect(getReturnValue("1234", ElementType.PIN, false)).toBe(undefined) + expect(getReturnValue("5066 9911 1111 1118", BaseElementType.CARD_NUMBER, false)).toBe("50669911XXXXXXXX") + expect(getReturnValue("123", BaseElementType.CVV, false)).toBe(undefined) + expect(getReturnValue("name", BaseElementType.CARDHOLDER_NAME, false)).toBe(undefined) + expect(getReturnValue("02", BaseElementType.EXPIRATION_MONTH, false)).toBe(undefined) + expect(getReturnValue("2025", BaseElementType.EXPIRATION_YEAR, false)).toBe(undefined) + expect(getReturnValue("1234", BaseElementType.PIN, false)).toBe(undefined) }) test("in DEV return data for all elements", () => { - expect(getReturnValue("4111 1111 1111 1111", ElementType.CARD_NUMBER, true)).toBe("4111111111111111") - expect(getReturnValue("123", ElementType.CVV, true)).toBe("123") - expect(getReturnValue("1234", ElementType.PIN, true)).toBe("1234") - expect(getReturnValue("name", ElementType.CARDHOLDER_NAME, true)).toBe("name") - expect(getReturnValue("02", ElementType.EXPIRATION_MONTH, true)).toBe("02") - expect(getReturnValue("2025", ElementType.EXPIRATION_YEAR, true)).toBe("2025") + expect(getReturnValue("4111 1111 1111 1111", BaseElementType.CARD_NUMBER, true)).toBe("4111111111111111") + expect(getReturnValue("123", BaseElementType.CVV, true)).toBe("123") + expect(getReturnValue("1234", BaseElementType.PIN, true)).toBe("1234") + expect(getReturnValue("name", BaseElementType.CARDHOLDER_NAME, true)).toBe("name") + expect(getReturnValue("02", BaseElementType.EXPIRATION_MONTH, true)).toBe("02") + expect(getReturnValue("2025", BaseElementType.EXPIRATION_YEAR, true)).toBe("2025") }) }) describe('bin data for for AMEX card number element type on CHANGE event', () => { test("in PROD return bin data only for card number element", () => { expect(detectCardType("3782 822463 10005")).toBe(CardType.AMEX) - expect(getReturnValue("3782 822463 10005", ElementType.CARD_NUMBER, false)).toBe("378282XXXXXXXXX") - expect(getReturnValue("3782 822", ElementType.CARD_NUMBER, false)).toBe("378282X") - expect(getReturnValue("123", ElementType.CVV, false)).toBe(undefined) - expect(getReturnValue("name", ElementType.CARDHOLDER_NAME, false)).toBe(undefined) - expect(getReturnValue("02", ElementType.EXPIRATION_MONTH, false)).toBe(undefined) - expect(getReturnValue("2025", ElementType.EXPIRATION_YEAR, false)).toBe(undefined) - expect(getReturnValue("1234", ElementType.PIN, false)).toBe(undefined) - expect(getReturnValue('4111 1111 1111 1111', ElementType.CARD_NUMBER, true)).toBe('4111111111111111'); - expect(getReturnValue('4111-1111-1111-1111', ElementType.CARD_NUMBER, true)).toBe('4111111111111111'); + expect(getReturnValue("3782 822463 10005", BaseElementType.CARD_NUMBER, false)).toBe("378282XXXXXXXXX") + expect(getReturnValue("3782 822", BaseElementType.CARD_NUMBER, false)).toBe("378282X") + expect(getReturnValue("123", BaseElementType.CVV, false)).toBe(undefined) + expect(getReturnValue("name", BaseElementType.CARDHOLDER_NAME, false)).toBe(undefined) + expect(getReturnValue("02", BaseElementType.EXPIRATION_MONTH, false)).toBe(undefined) + expect(getReturnValue("2025", BaseElementType.EXPIRATION_YEAR, false)).toBe(undefined) + expect(getReturnValue("1234", BaseElementType.PIN, false)).toBe(undefined) + expect(getReturnValue('4111 1111 1111 1111', BaseElementType.CARD_NUMBER, true)).toBe('4111111111111111'); + expect(getReturnValue('4111-1111-1111-1111', BaseElementType.CARD_NUMBER, true)).toBe('4111111111111111'); }) test("in DEV return data for all elements", () => { - expect(getReturnValue("3782 822463 10005", ElementType.CARD_NUMBER, true)).toBe("378282246310005") - expect(getReturnValue("123", ElementType.CVV, true)).toBe("123") - expect(getReturnValue("1234", ElementType.PIN, true)).toBe("1234") - expect(getReturnValue("name", ElementType.CARDHOLDER_NAME, true)).toBe("name") - expect(getReturnValue("02", ElementType.EXPIRATION_MONTH, true)).toBe("02") - expect(getReturnValue("2025", ElementType.EXPIRATION_YEAR, true)).toBe("2025") + expect(getReturnValue("3782 822463 10005", BaseElementType.CARD_NUMBER, true)).toBe("378282246310005") + expect(getReturnValue("123", BaseElementType.CVV, true)).toBe("123") + expect(getReturnValue("1234", BaseElementType.PIN, true)).toBe("1234") + expect(getReturnValue("name", BaseElementType.CARDHOLDER_NAME, true)).toBe("name") + expect(getReturnValue("02", BaseElementType.EXPIRATION_MONTH, true)).toBe("02") + expect(getReturnValue("2025", BaseElementType.EXPIRATION_YEAR, true)).toBe("2025") }) }) @@ -760,6 +760,20 @@ describe('checkAndSetForCustomUrl', () => { const isValid = isValidURL(config.options.customElementsURL); expect(isValid).toEqual(false); }); + + it('should reject a near-miss scheme that merely starts with "https"', () => { + // `httpsx://` parses as a valid URL with scheme `httpsx`, but it is not + // TLS and must be rejected. + expect(isValidURL('httpsx://js.skyflow.com')).toEqual(false); + }); + + it('should reject an http (non-TLS) url', () => { + expect(isValidURL('http://js.skyflow.com')).toEqual(false); + }); + + it('should accept an uppercase HTTPS scheme', () => { + expect(isValidURL('HTTPS://js.skyflow.com')).toEqual(true); + }); }); diff --git a/tests/utils/jwt-utils.test.js b/packages/skyflow-js/tests/utils/jwt-utils.test.js similarity index 89% rename from tests/utils/jwt-utils.test.js rename to packages/skyflow-js/tests/utils/jwt-utils.test.js index 332e11c59..e07b78a80 100644 --- a/tests/utils/jwt-utils.test.js +++ b/packages/skyflow-js/tests/utils/jwt-utils.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import isTokenValid from "../../src/utils/jwt-utils"; +import isTokenValid from "@core/utils/jwt-utils"; jest.mock('jwt-decode', () => () => ({exp: 123})) describe('Validation token', () => { diff --git a/packages/skyflow-js/tests/utils/logs-helper.test.js b/packages/skyflow-js/tests/utils/logs-helper.test.js new file mode 100644 index 000000000..83c7d2ced --- /dev/null +++ b/packages/skyflow-js/tests/utils/logs-helper.test.js @@ -0,0 +1,28 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import { getStoredSdkVersion } from '@core/utils/logs-helper'; + +// In this package SDK_NAME resolves to 'skyflow-js' (jest.setup injects it from +// package.json), so getStoredSdkVersion must keep honouring the legacy global +// `sdk_version` key that skyflow-react-js writes. +describe('Utils/logs-helper getStoredSdkVersion (skyflow-js)', () => { + beforeEach(() => { + localStorage.clear(); + }); + + test('returns empty string when nothing is stored', () => { + expect(getStoredSdkVersion()).toBe(''); + }); + + test('honours the legacy global sdk_version key (React wrapper override)', () => { + localStorage.setItem('sdk_version', 'skyflow-react-js@9.9.9'); + expect(getStoredSdkVersion()).toBe('skyflow-react-js@9.9.9'); + }); + + test('per-package namespaced key takes precedence over the legacy global key', () => { + localStorage.setItem('sdk_version', 'skyflow-react-js@9.9.9'); + localStorage.setItem('sdk_version:skyflow-js', 'skyflow-react-js@8.8.8'); + expect(getStoredSdkVersion()).toBe('skyflow-react-js@8.8.8'); + }); +}); diff --git a/packages/skyflow-js/tests/utils/safe-merge.test.ts b/packages/skyflow-js/tests/utils/safe-merge.test.ts new file mode 100644 index 000000000..f17547f08 --- /dev/null +++ b/packages/skyflow-js/tests/utils/safe-merge.test.ts @@ -0,0 +1,83 @@ +import { safeMerge } from "@core/utils/safe-merge"; + +// Clean up any accidental prototype pollution so a failure here cannot leak +// into unrelated tests. +afterEach(() => { + delete (Object.prototype as any).polluted; +}); + +describe("safeMerge — behaviour preservation (matches lodash/merge)", () => { + test("deep-merges nested objects", () => { + const target: any = { a: { x: 1 }, b: 2 }; + const result = safeMerge(target, { a: { y: 3 }, c: 4 }); + expect(result).toEqual({ a: { x: 1, y: 3 }, b: 2, c: 4 }); + }); + + test("mutates and returns the same target reference (in-place)", () => { + const target: any = { a: 1 }; + const result = safeMerge(target, { b: 2 }); + expect(result).toBe(target); + expect(target).toEqual({ a: 1, b: 2 }); + }); + + test("preserves arrays", () => { + const target: any = { list: [1, 2] }; + const result = safeMerge(target, { list: [9] }); + // lodash merges arrays index-wise; safeMerge delegates to it unchanged. + expect(result).toEqual({ list: [9, 2] }); + }); + + test("handles null / undefined / primitive sources without throwing", () => { + const target: any = { a: 1 }; + expect(safeMerge(target, null)).toEqual({ a: 1 }); + expect(safeMerge(target, undefined)).toEqual({ a: 1 }); + }); + + test("supports multiple sources", () => { + const target: any = {}; + const result = safeMerge(target, { a: 1 }, { b: 2 }); + expect(result).toEqual({ a: 1, b: 2 }); + }); +}); + +describe("safeMerge — prototype-pollution guard", () => { + test("does not pollute Object.prototype via an own __proto__ key", () => { + // JSON.parse creates `__proto__` as an OWN enumerable key (the realistic + // attacker vector); an object literal would set the prototype instead. + const malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}'); + const target: any = {}; + safeMerge(target, malicious); + + expect(({} as any).polluted).toBeUndefined(); + expect((Object.prototype as any).polluted).toBeUndefined(); + }); + + test("does not pollute via constructor.prototype path", () => { + const malicious = JSON.parse( + '{"constructor": {"prototype": {"polluted": "yes"}}}' + ); + const target: any = {}; + safeMerge(target, malicious); + + expect(({} as any).polluted).toBeUndefined(); + }); + + test("does not pollute via a nested __proto__ key", () => { + const malicious = JSON.parse('{"a": {"__proto__": {"polluted": "yes"}}}'); + const target: any = {}; + safeMerge(target, malicious); + + expect(({} as any).polluted).toBeUndefined(); + }); + + test("still merges legitimate keys while dropping forbidden ones", () => { + const malicious = JSON.parse( + '{"safeCol": "value", "__proto__": {"polluted": "yes"}}' + ); + const target: any = { existing: 1 }; + const result = safeMerge(target, malicious); + + expect(result).toEqual({ existing: 1, safeCol: "value" }); + expect(({} as any).polluted).toBeUndefined(); + }); +}); diff --git a/tests/utils/validators.test.js b/packages/skyflow-js/tests/utils/validators.test.js similarity index 99% rename from tests/utils/validators.test.js rename to packages/skyflow-js/tests/utils/validators.test.js index c1ef67633..c43f5306b 100644 --- a/tests/utils/validators.test.js +++ b/packages/skyflow-js/tests/utils/validators.test.js @@ -1,8 +1,8 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { CardType, SDK_VERSION } from '../../src/core/constants'; -import SKYFLOW_ERROR_CODE from '../../src/utils/constants'; +import { CardType, SDK_VERSION } from '@core/constants'; +import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import { detectCardType, isValidRegExp, diff --git a/packages/skyflow-js/tsconfig.json b/packages/skyflow-js/tsconfig.json new file mode 100644 index 000000000..7f617c1e6 --- /dev/null +++ b/packages/skyflow-js/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "types" + }, + "include": ["src","../../core/custom.d.ts","typings.d.ts"], + "exclude": ["node_modules", "dist", "tests/*.test.*"] +} diff --git a/packages/skyflow-js/typings.d.ts b/packages/skyflow-js/typings.d.ts new file mode 100644 index 000000000..384f5c802 --- /dev/null +++ b/packages/skyflow-js/typings.d.ts @@ -0,0 +1,9 @@ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +declare module '*.json'; + +// SDK telemetry identity, injected at build time (webpack DefinePlugin) and in +// tests (jest setupFiles). Each package supplies its own name/version. +declare const SDK_NAME: string; +declare const SDK_VERSION: string; diff --git a/packages/skyflow-js/webpack.dev.js b/packages/skyflow-js/webpack.dev.js new file mode 100644 index 000000000..d105b088e --- /dev/null +++ b/packages/skyflow-js/webpack.dev.js @@ -0,0 +1,11 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared dev-server factory (webpack/dev.js). +// To hit a real vault locally, add a proxy block below (kept local, never +// committed), e.g.: +// proxy: { '/vault': { target: 'https://', pathRewrite: { '^/vault': '' }, secure: false, changeOrigin: true } }, +module.exports = require('../../webpack/dev.js')(__dirname, { + port: 3040, + analyzerPort: 8881, +}); diff --git a/packages/skyflow-js/webpack.iframe.js b/packages/skyflow-js/webpack.iframe.js new file mode 100644 index 000000000..060264ae2 --- /dev/null +++ b/packages/skyflow-js/webpack.iframe.js @@ -0,0 +1,5 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared iframe factory (webpack/iframe.js). +module.exports = require('../../webpack/iframe.js')(__dirname); diff --git a/packages/skyflow-js/webpack.skyflow-browser.js b/packages/skyflow-js/webpack.skyflow-browser.js new file mode 100644 index 000000000..d723bbbd4 --- /dev/null +++ b/packages/skyflow-js/webpack.skyflow-browser.js @@ -0,0 +1,5 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared browser-SDK factory (webpack/browser.js). +module.exports = require('../../webpack/browser.js')(__dirname); diff --git a/packages/skyflow-js/webpack.skyflow-node.js b/packages/skyflow-js/webpack.skyflow-node.js new file mode 100644 index 000000000..b80bcb1d8 --- /dev/null +++ b/packages/skyflow-js/webpack.skyflow-node.js @@ -0,0 +1,6 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Thin wrapper over the shared node-SDK factory (webpack/node.js). +// UMD global for skyflow-js is `Skyflow`. +module.exports = require('../../webpack/node.js')(__dirname, { library: 'Skyflow' }); diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 9fe7e66b3..533077ea6 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -1,19 +1,36 @@ Version=$1 +PACKAGE_DIR=$2 +SHA=$3 SEMVER=$Version -if [ -z $2 ] +if [ -z "$PACKAGE_DIR" ] then - echo "Bumping package version to $1" + echo "Usage: bump_version.sh [sha]" >&2 + echo " e.g. bump_version.sh 2.7.9 packages/skyflow-js" >&2 + exit 1 +fi - sed -E "s/\"version\": .+/\"version\": \"$SEMVER\",/g" package.json > tempfile && cat tempfile > package.json && rm -f tempfile +MANIFEST="$PACKAGE_DIR/package.json" + +if [ ! -f "$MANIFEST" ] +then + echo "No package.json found at $MANIFEST" >&2 + exit 1 +fi + +if [ -z "$SHA" ] +then + echo "Bumping $PACKAGE_DIR version to $1" + + sed -E "s/\"version\": .+/\"version\": \"$SEMVER\",/g" "$MANIFEST" > tempfile && cat tempfile > "$MANIFEST" && rm -f tempfile echo -------------------------- - echo "Done, Package now at $1" + echo "Done, $PACKAGE_DIR now at $1" else - echo "Bumping package version to $1-dev.$2" + echo "Bumping $PACKAGE_DIR version to $1-dev.$SHA" - sed -E "s/\"version\": .+/\"version\": \"$SEMVER-dev.$2\",/g" package.json > tempfile && cat tempfile > package.json && rm -f tempfile + sed -E "s/\"version\": .+/\"version\": \"$SEMVER-dev.$SHA\",/g" "$MANIFEST" > tempfile && cat tempfile > "$MANIFEST" && rm -f tempfile echo -------------------------- - echo "Done, Package now at $1-dev.$2" -fi \ No newline at end of file + echo "Done, $PACKAGE_DIR now at $1-dev.$SHA" +fi diff --git a/src/core/external/collect/collect-container.ts b/src/core/external/collect/collect-container.ts deleted file mode 100644 index 525370158..000000000 --- a/src/core/external/collect/collect-container.ts +++ /dev/null @@ -1,553 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -import bus from 'framebus'; -import iframer, { setAttributes, getIframeSrc, setStyles } from '../../../iframe-libs/iframer'; -import deepClone from '../../../libs/deep-clone'; -import { - formatValidations, formatOptions, validateElementOptions, -} from '../../../libs/element-options'; -import SkyflowError from '../../../libs/skyflow-error'; -import uuid from '../../../libs/uuid'; -import { ContainerType } from '../../../skyflow'; -import { - Context, MessageType, - CollectElementInput, - CollectElementOptions, - CollectResponse, - ICollectOptions, - UploadFilesResponse, - ContainerOptions, - ErrorType, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import logs from '../../../utils/logs'; -import { printLog, parameterizedString } from '../../../utils/logs-helper'; -import { - validateCollectElementInput, validateInitConfig, - validateAdditionalFieldsInCollect, - validateUpsertOptions, - validateBooleanOptions, -} from '../../../utils/validators'; -import { - COLLECT_FRAME_CONTROLLER, - CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, - ELEMENTS, FRAME_ELEMENT, - COLLECT_TYPES, - ElementType, -} from '../../constants'; -import Container from '../common/container'; -import CollectElement from './collect-element'; -import EventEmitter from '../../../event-emitter'; -import properties from '../../../properties'; -import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; - -export interface ICollectElement { - elementType: ElementType; - elementName: string; - name: string; - table?: string; - column?: string; - sensitive?: boolean; - replacePattern?: RegExp; - mask?: string[]; - value?: string; - isMounted: boolean; - [key: string]: unknown; -} - -export interface ElementGroupItem extends CollectElementInput, CollectElementOptions { - elementType: ElementType; - name?: string; - accept?: string[]; - elementName?: string; -} - -export interface ElementGroup { - rows: Array<{ - elements: Array; - }>; -} - -const CLASS_NAME = 'CollectContainer'; -class CollectContainer extends Container { - #containerId: string; - - #elements: Record = {}; - - #metaData: Metadata; - - #context: Context; - - #skyflowElements: Array; - - type:string = ContainerType.COLLECT; - - #eventEmitter: EventEmitter; - - #isMounted: boolean = false; - - #isSkyflowFrameReady: boolean = false; - - #customErrorMessages: Partial> = {}; - - constructor( - metaData: Metadata, - skyflowElements: Array, - context: Context, - options?: ContainerOptions, - ) { - super(); - this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; - this.#containerId = uuid(); - this.#metaData = { - ...metaData, - clientJSON: { - ...metaData.clientJSON, - config: { - ...metaData.clientJSON.config, - options: { - ...metaData.clientJSON.config?.options, - ...options, - }, - }, - }, - }; - this.#skyflowElements = skyflowElements; - this.#context = context; - this.#eventEmitter = new EventEmitter(); - - const clientDomain = this.#metaData.clientDomain || ''; - const iframe = iframer({ - name: `${COLLECT_FRAME_CONTROLLER}:${this.#containerId}:${this.#context.logLevel}:${btoa(clientDomain)}`, - referrer: clientDomain, - }); - setAttributes(iframe, { - src: getIframeSrc(), - }); - setStyles(iframe, { ...CONTROLLER_STYLES }); - printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - this.#isMounted = true; - } - - create = (input: CollectElementInput, options: CollectElementOptions = { - required: false, - }): CollectElement => { - validateCollectElementInput(input, this.#context.logLevel); - const validations = formatValidations(input.validations); - const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); - - const elementGroup: ElementGroup = { - rows: [{ - elements: [{ - elementType: input.type, - name: input.column, - accept: options.allowedFileType, - ...input, - ...formattedOptions, - validations, - }], - }], - }; - - return this.#createMultipleElement(elementGroup, true); - }; - - setError(errors: Partial>) { - this.#customErrorMessages = errors; - } - - #createMultipleElement = ( - multipleElements: ElementGroup, - isSingleElementAPI: boolean = false, - ): CollectElement => { - const elements: any[] = []; - const tempElements = deepClone(multipleElements); - - tempElements.rows.forEach((row) => { - row.elements.forEach((element) => { - const options = element; - const { elementType } = options; - validateElementOptions(elementType, options); - - options.sensitive = options.sensitive || ELEMENTS[elementType].sensitive; - options.replacePattern = options.replacePattern || ELEMENTS[elementType].replacePattern; - options.mask = options.mask || ELEMENTS[elementType].mask; - - // options.elementName = `${options.table}.${options.name}:${btoa(uuid())}`; - // options.elementName = (options.table && options.name) ? `${options.elementType}:${btoa( - // options.elementName, - // )}` : `${options.elementType}:${btoa(uuid())}`; - - options.isMounted = false; - - if ( - options.elementType === ELEMENTS.radio.name - || options.elementType === ELEMENTS.checkbox.name - ) { - options.elementName = `${options.elementName}:${btoa(options.value)}`; - } - - options.elementName = `${FRAME_ELEMENT}:${options.elementType}:${btoa(uuid())}`; - options.label = element.label; - options.skyflowID = element.skyflowID; - - elements.push(options); - }); - }); - - tempElements.elementName = isSingleElementAPI - ? elements[0].elementName - : `${FRAME_ELEMENT}:group:${btoa(tempElements.name)}`; - - if ( - isSingleElementAPI - && !this.#elements[elements[0].elementName] - && this.#hasElementName(elements[0].name) - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); - } - - let element = this.#elements[tempElements.elementName]; - if (element) { - if (isSingleElementAPI) { - element.updateElementGroup(elements[0]); - } else { - element.updateElementGroup(tempElements); - } - } else { - const elementId = uuid(); - element = new CollectElement( - elementId, - tempElements, - this.#metaData, - { - containerId: this.#containerId, - isMounted: this.#isMounted, - type: this.type, - }, - isSingleElementAPI, - this.#destroyCallback, - this.#updateCallback, - this.#context, - this.#eventEmitter, - ); - this.#elements[tempElements.elementName] = element; - this.#skyflowElements[elementId] = element; - } - - if (!isSingleElementAPI) { - elements.forEach((iElement) => { - const name = iElement.elementName; - if (!this.#elements[name]) { - this.#elements[name] = this.create(iElement.elementType, iElement); - } else { - this.#elements[name].updateElementGroup(iElement); - } - }); - } - return element; - }; - - #removeElement = (elementName: string) => { - Object.keys(this.#elements).forEach((element) => { - if (element === elementName) delete this.#elements[element]; - }); - }; - - #destroyCallback = (elementNames: string[]) => { - elementNames.forEach((elementName) => { - this.#removeElement(elementName); - }); - }; - - #updateCallback = (elements: any[]) => { - elements.forEach((element) => { - if (this.#elements[element.elementName]) { - this.#elements[element.elementName].updateElementGroup(element); - } - }); - }; - - #hasElementName = (name: string) => { - const tempElements = Object.keys(this.#elements); - for (let i = 0; i < tempElements.length; i += 1) { - if (atob(tempElements[i].split(':')[2]) === name) { - return true; - } - } - return false; - }; - - collect = (options: ICollectOptions = { tokens: true }): Promise => { - this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; - if (this.#isSkyflowFrameReady) { - // eslint-disable-next-line @typescript-eslint/no-shadow - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (Object.keys(this.#elements).length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); - } - this.#removeStaleElements(); - const collectElements = Object.values(this.#elements); - const elementIds = Object.keys(this.#elements) - .map((element) => ({ frameId: element, elementId: element })); - collectElements.forEach((element) => { - if (!element.isMounted()) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - element.isValidElement(); - }); - if (Object.prototype.hasOwnProperty.call(options, 'tokens') && !validateBooleanOptions(options.tokens)) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); - } - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - errorMessages: this.#customErrorMessages, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - resolve(data); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err: any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (Object.keys(this.#elements).length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); - } - this.#removeStaleElements(); - const collectElements = Object.values(this.#elements); - const elementIds = Object.keys(this.#elements) - .map((element) => ({ frameId: element, elementId: element })); - collectElements.forEach((element) => { - if (!element.isMounted()) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - element.isValidElement(); - }); - if (Object.prototype.hasOwnProperty.call(options, 'tokens') && !validateBooleanOptions(options.tokens)) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); - } - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#containerId, () => { - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - errorMessages: this.#customErrorMessages, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - resolve(data); - } - }, - ); - }); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - }; - - uploadFiles = (options?: ICollectOptions): Promise => { - this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; - if (this.#isSkyflowFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (Object.keys(this.#elements).length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); - } - this.#removeStaleElements(); - const fileElements = Object.values(this.#elements); - const elementIds = Object.keys(this.#elements); - fileElements.forEach((element) => { - if (!element.isMounted()) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - element.isValidElement(); - }); - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.FILE_UPLOAD, - ...options, - elementIds, - containerId: this.#containerId, - errorMessages: this.#customErrorMessages, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - resolve(data); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - } - return new Promise((resolve, reject) => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#containerId, () => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (Object.keys(this.#elements).length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COLLECT, [], true); - } - this.#removeStaleElements(); - const fileElements = Object.values(this.#elements); - const elementIds = Object.keys(this.#elements); - fileElements.forEach((element) => { - if (!element.isMounted()) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - element.isValidElement(); - }); - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.FILE_UPLOAD, - ...options, - elementIds, - containerId: this.#containerId, - errorMessages: this.#customErrorMessages, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - resolve(data); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - }); - }; - - #removeStaleElements = (): void => { - try { - if (this.#hasNoElements()) return; - - const mountedIframeIds = this.#getMountedIframeIds(); - if (!mountedIframeIds.length) return; - - this.#removeUnmountedElements(mountedIframeIds); - } catch (error: unknown) { - printLog(`${error}`, MessageType.LOG, this.#context.logLevel); - } - }; - - #hasNoElements = (): boolean => Object.keys(this.#elements).length === 0; - - #getMountedIframeIds = (): string[] => { - const body = document?.body; - if (!body) return []; - - const iframes = body.getElementsByTagName('iframe'); - if (!iframes?.length) return []; - - return Array.from(iframes).map((iframe) => iframe.id); - }; - - #removeUnmountedElements = (mountedIframeIds: string[]): void => { - Object.entries(this.#elements).forEach(([key, element]) => { - if (this.#shouldRemoveElement(element, mountedIframeIds)) { - delete this.#elements[key]; - } - }); - }; - - #shouldRemoveElement = ( - element: CollectElement, - mountedIframeIds: string[], - ): boolean => ( - element.isMounted() - && !mountedIframeIds.includes(element.iframeName()) - ); -} -export default CollectContainer; diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts deleted file mode 100644 index a52484edb..000000000 --- a/src/core/external/collect/compose-collect-container.ts +++ /dev/null @@ -1,602 +0,0 @@ -/* eslint-disable no-plusplus */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* -Copyright (c) 2023 Skyflow, Inc. -*/ -import bus from 'framebus'; -import sum from 'lodash/sum'; -import EventEmitter from '../../../event-emitter'; -import iframer, { setAttributes, getIframeSrc, setStyles } from '../../../iframe-libs/iframer'; -import deepClone from '../../../libs/deep-clone'; -import { - formatValidations, formatOptions, validateElementOptions, getElements, -} from '../../../libs/element-options'; -import SkyflowError from '../../../libs/skyflow-error'; -import uuid from '../../../libs/uuid'; -import properties from '../../../properties'; -import { ContainerType } from '../../../skyflow'; -import { - Context, MessageType, - CollectElementInput, - CollectElementOptions, - ICollectOptions, - CollectResponse, - InputStyles, - ErrorTextStyles, - ContainerOptions, - UploadFilesResponse, - ErrorMessages, - ErrorType, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import logs from '../../../utils/logs'; -import { printLog, parameterizedString } from '../../../utils/logs-helper'; -import { - validateCollectElementInput, validateInitConfig, validateAdditionalFieldsInCollect, - validateUpsertOptions, -} from '../../../utils/validators'; -import { - COLLECT_FRAME_CONTROLLER, - CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, - ELEMENTS, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, - COLLECT_TYPES, -} from '../../constants'; -import Container from '../common/container'; -import CollectElement from './collect-element'; -import ComposableElement from './compose-collect-element'; -import { ElementGroup, ElementGroupItem } from './collect-container'; -import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; -import Client from '../../../client'; - -export interface ComposableElementGroup extends ElementGroup { - styles: InputStyles; - errorTextStyles: ErrorTextStyles; -} - -const CLASS_NAME = 'CollectContainer'; -class ComposableContainer extends Container { - #containerId: string; - - #elements: Record = {}; - - #metaData: Metadata; - - #elementGroup: ComposableElementGroup = { rows: [], styles: {}, errorTextStyles: {} }; - - #elementsList: Array = []; - - #context:Context; - - #skyflowElements: Array; - - #eventEmitter: EventEmitter; - - #isMounted: boolean = false; - - #options: ContainerOptions; - - #containerElement:any; - - type:string = ContainerType.COMPOSABLE; - - #containerMounted: boolean = false; - - #tempElements: any = {}; - - #clientDomain: string = ''; - - #isComposableFrameReady: boolean = false; - - #shadowRoot: ShadowRoot | null = null; - - #iframeID: string = ''; - - #getSkyflowBearerToken: () => Promise | undefined; - - #customErrorMessages: Partial> = {}; - - constructor( - metaData: Metadata, - skyflowElements: Array, - context: Context, - options: ContainerOptions, - ) { - super(); - this.#containerId = uuid(); - this.#metaData = { - ...metaData, - clientJSON: { - ...metaData.clientJSON, - config: { - ...metaData.clientJSON.config, - options: { - ...metaData.clientJSON.config?.options, - ...options, - }, - }, - }, - }; - this.#getSkyflowBearerToken = metaData.getSkyflowBearerToken; - this.#skyflowElements = skyflowElements; - this.#context = context; - this.#options = options; - this.#eventEmitter = new EventEmitter(); - - this.#clientDomain = this.#metaData.clientDomain || ''; - const iframe = iframer({ - name: `${COLLECT_FRAME_CONTROLLER}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`, - referrer: this.#clientDomain, - }); - setAttributes(iframe, { - src: getIframeSrc(), - }); - setStyles(iframe, { ...CONTROLLER_STYLES }); - printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#containerMounted = true; - this.#updateListeners(); - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { - printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - callback({ - client: this.#metaData.clientJSON, - context, - }); - this.#isComposableFrameReady = true; - }); - } - - create = (input: CollectElementInput, options: CollectElementOptions = { - required: false, - }): ComposableElement => { - validateCollectElementInput(input, this.#context.logLevel); - const validations = formatValidations(input.validations); - const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); - // let elementName; - // elementName = `${input.table}.${input.column}:${btoa(uuid())}`; - // elementName = (input.table && input.column) ? `${input.type}:${btoa( - // elementName, - // )}` : ; - - const elementName = `${FRAME_ELEMENT}:${input.type}:${btoa(uuid())}`; - - this.#elementsList.push({ - elementType: input.type, - name: input.column, - ...input, - ...formattedOptions, - validations, - elementName, - }); - const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`; - this.#iframeID = controllerIframeName; - return new ComposableElement( - elementName, this.#eventEmitter, controllerIframeName, - { ...this.#metaData, type: input.type }, - ); - }; - - setError(errors: Partial>) { - this.#customErrorMessages = errors; - } - - #createMultipleElement = ( - multipleElements: ComposableElementGroup, - isSingleElementAPI: boolean = false, - ): ComposableContainer => { - const elements: any[] = []; - this.#tempElements = deepClone(multipleElements); - this.#tempElements.rows.forEach((row) => { - row.elements.forEach((element) => { - const options = element; - const { elementType } = options; - validateElementOptions(elementType, options); - - options.sensitive = options.sensitive || ELEMENTS[elementType].sensitive; - options.replacePattern = options.replacePattern || ELEMENTS[elementType].replacePattern; - options.mask = options.mask || ELEMENTS[elementType].mask; - - options.isMounted = false; - - options.label = element.label; - options.skyflowID = element.skyflowID; - - elements.push(options); - }); - }); - - this.#tempElements.elementName = isSingleElementAPI - ? elements[0].elementName - : `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}`; - if ( - isSingleElementAPI - && !this.#elements[elements[0].elementName] - && this.#hasElementName(elements[0].name) - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); - } - - let element = this.#elements[this.#tempElements.elementName]; - if (element) { - if (isSingleElementAPI) { - element.update(elements[0]); - } else { - element.update(this.#tempElements); - } - } else { - const elementId = uuid(); - element = new CollectElement( - elementId, - this.#tempElements, - this.#metaData, - { - containerId: this.#containerId, - isMounted: this.#containerMounted, - type: this.type, - }, - true, - this.#destroyCallback, - this.#updateCallback, - this.#context, - this.#eventEmitter, - ); - this.#elements[this.#tempElements.elementName] = element; - this.#skyflowElements[elementId] = element; - } - return element; - }; - - #removeElement = (elementName: string) => { - Object.keys(this.#elements).forEach((element) => { - if (element === elementName) delete this.#elements[element]; - }); - }; - - #destroyCallback = (elementNames: string[]) => { - elementNames.forEach((elementName) => { - this.#removeElement(elementName); - }); - }; - - #updateCallback = (elements: any[]) => { - elements.forEach((element) => { - if (this.#elements[element.elementName]) { - this.#elements[element.elementName].update(element); - } - }); - }; - - #hasElementName = (name: string) => { - const tempElements = Object.keys(this.#elements); - for (let i = 0; i < tempElements.length; i += 1) { - if (atob(tempElements[i].split(':')[2]) === name) { - return true; - } - } - return false; - }; - - on = (eventName:string, handler:Function) => { - if (!Object.values(ELEMENT_EVENTS_TO_CLIENT).includes(eventName)) { - throw new SkyflowError( - SKYFLOW_ERROR_CODE.INVALID_EVENT_LISTENER, - [], - true, - ); - } - if (!handler) { - throw new SkyflowError( - SKYFLOW_ERROR_CODE.MISSING_HANDLER_IN_EVENT_LISTENER, - [], - true, - ); - } - if (typeof handler !== 'function') { - throw new SkyflowError( - SKYFLOW_ERROR_CODE.INVALID_HANDLER_IN_EVENT_LISTENER, - [], - true, - ); - } - - this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.SUBMIT, () => { - handler(); - }); - }; - - mount = (domElement: HTMLElement | string) => { - if (!domElement) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, - ['CollectElement'], true); - } - - const { layout } = this.#options; - if (sum(layout) !== this.#elementsList.length) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, [], true); - } - let count = 0; - layout.forEach((rowCount, index) => { - this.#elementGroup.rows = [ - ...this.#elementGroup.rows, - { elements: [] }, - ]; - for (let i = 0; i < rowCount; i++) { - this.#elementGroup.rows[index].elements.push( - this.#elementsList[count], - ); - count++; - } - }); - if (this.#options.styles) { - this.#elementGroup.styles = { - ...this.#options.styles, - }; - } - if (this.#options.errorTextStyles) { - this.#elementGroup.errorTextStyles = { - ...this.#options.errorTextStyles, - }; - } - - if (this.#containerMounted) { - this.#containerElement = this.#createMultipleElement(this.#elementGroup, false); - this.#containerElement.mount(domElement); - this.#isMounted = true; - } - this.#elementsList.forEach((element) => { - this.#eventEmitter.on(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, (data, callback) => { - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#emitEvent( - `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, - { - elementName: element.name, - data: { - type: COLLECT_TYPES.FILE_UPLOAD, - containerId: this.#containerId, - }, - clientConfig: { - vaultURL: this.#metaData?.clientJSON?.config?.vaultURL, - vaultID: this.#metaData?.clientJSON?.config?.vaultID, - authToken, - }, - options: { - ...data?.options, - }, - errorMessages: this.#customErrorMessages, - }, - ); - }).catch((err:any) => { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - callback(err); - }); - }); - }); - if (domElement instanceof HTMLElement - && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { - this.#shadowRoot = domElement.getRootNode() as ShadowRoot; - } else if (typeof domElement === 'string') { - const element = document.getElementById(domElement); - if (element && element.getRootNode() instanceof ShadowRoot) { - this.#shadowRoot = element.getRootNode() as ShadowRoot; - } - } - if (this.#shadowRoot !== null) { - this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { - this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); - }); - this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); - } - }; - - unmount = () => { - this.#containerElement.unmount(); - }; - - collect = (options: ICollectOptions = { tokens: true }) : - Promise => new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); - }); - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); - } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName ?? '', - }); - }); - const client = Client.fromJSON(this.#metaData.clientJSON) as any; - const clientId = client.toJSON()?.metaData?.uuid || ''; - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { - data: { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - clientConfig: { - vaultURL: this.#metaData.clientJSON.config.vaultURL, - vaultID: this.#metaData.clientJSON.config.vaultID, - authToken, - }, - errorMessages: this.#customErrorMessages, - }); - }).catch((err:any) => { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - }); - window.addEventListener('message', (event) => { - if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (event?.data?.type - === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.#containerId) { - const data = event.data.data; - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else if (data?.records) { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - resolve(data); - } else { - printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); - reject(data); - } - } - } - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - - #emitEvent = (eventName: string, options?: Record, callback?: any) => { - if (this.#shadowRoot) { - const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; - if (iframe?.contentWindow) { - iframe.contentWindow.postMessage({ - name: eventName, - ...options, - }, properties.IFRAME_SECURE_ORIGIN); - } - } else { - const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; - if (iframe?.contentWindow) { - iframe.contentWindow.postMessage({ - name: eventName, - ...options, - }, properties.IFRAME_SECURE_ORIGIN); - } - } - }; - - uploadFiles = (options: ICollectOptions): - Promise => new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } - const elementIds:{ frameId:string, elementId:string }[] = []; - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName ?? '', - }); - }); - const client = Client.fromJSON(this.#metaData.clientJSON) as any; - const clientId = client.toJSON()?.metaData?.uuid || ''; - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { - data: { - type: COLLECT_TYPES.FILE_UPLOAD, - ...options, - // tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - clientConfig: { - vaultURL: this.#metaData.clientJSON.config.vaultURL, - vaultID: this.#metaData.clientJSON.config.vaultID, - authToken, - }, - errorMessages: this.#customErrorMessages, - }); - window.addEventListener('message', (event) => { - if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (event.data?.type - === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.#containerId) { - const data = event.data.data; - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else if (data?.fileUploadResponse) { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - resolve(data); - } else { - printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); - reject(data); - } - } - } - }); - }).catch((err:any) => { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - }); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - - #updateListeners = () => { - this.#eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS, (data) => { - let elementIndex; - const elementList = this.#elementsList.map((element, index) => { - if (element.elementName === data.elementName) { - elementIndex = index; - return { - elementName: element.elementName, - ...data.elementOptions, - }; - } - return element; - }); - - if (this.#containerElement) { - this.#containerElement.updateElement({ - ...elementList[elementIndex], - }); - } - }); - }; -} -export default ComposableContainer; diff --git a/src/core/external/common/container.ts b/src/core/external/common/container.ts deleted file mode 100644 index f3a1efb57..000000000 --- a/src/core/external/common/container.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -abstract class Container { -} - -export default Container; diff --git a/src/core/external/reveal/composable-reveal-container.ts b/src/core/external/reveal/composable-reveal-container.ts deleted file mode 100644 index 864317ac7..000000000 --- a/src/core/external/reveal/composable-reveal-container.ts +++ /dev/null @@ -1,493 +0,0 @@ -/* eslint-disable no-plusplus */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* -Copyright (c) 2023 Skyflow, Inc. -*/ -import bus from 'framebus'; -import sum from 'lodash/sum'; -import EventEmitter from '../../../event-emitter'; -import iframer, { setAttributes, getIframeSrc, setStyles } from '../../../iframe-libs/iframer'; -import deepClone from '../../../libs/deep-clone'; -import SkyflowError from '../../../libs/skyflow-error'; -import uuid from '../../../libs/uuid'; -import properties from '../../../properties'; -import { ContainerType } from '../../../skyflow'; -import { - Context, MessageType, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import logs from '../../../utils/logs'; -import { printLog, parameterizedString } from '../../../utils/logs-helper'; -import { - validateInitConfig, - validateInputFormatOptions, - validateRevealElementRecords, -} from '../../../utils/validators'; -import { - COLLECT_FRAME_CONTROLLER, - CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, - FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, - COMPOSABLE_REVEAL, - REVEAL_TYPES, - CUSTOM_ERROR_MESSAGES, -} from '../../constants'; -import Container from '../common/container'; - -import ComposableRevealElement from './composable-reveal-element'; -import { - ContainerOptions, ErrorMessages, ErrorType, RevealElementInput, RevealResponse, -} from '../../../index-node'; -import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; -import ComposableRevealInternalElement from './composable-reveal-internal'; -import { formatRevealElementOptions } from '../../../utils/helpers'; -import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; -import ComposableContainer, { ComposableElementGroup } from '../collect/compose-collect-container'; - -const CLASS_NAME = 'ComposableRevealContainer'; -class ComposableRevealContainer extends Container { - #containerId: string; - - #elements: Record = {}; - - #metaData: Metadata; - - #elementGroup: any = { rows: [] }; - - #elementsList:any = []; - - #context: Context; - - #skyflowElements: Array; - - #eventEmitter: EventEmitter; - - #isMounted: boolean = false; - - #options: any; - - #containerElement:any; - - type:string = ContainerType.COMPOSE_REVEAL; - - #containerMounted: boolean = false; - - #tempElements: any = {}; - - #clientDomain: string = ''; - - #isComposableFrameReady: boolean = false; - - #shadowRoot: ShadowRoot | null = null; - - #iframeID: string = ''; - - #revealRecords: IRevealElementInput[] = []; - - #getSkyflowBearerToken: () => Promise | undefined; - - #customErrorMessages: Partial> = {}; - - constructor( - metaData: Metadata, - skyflowElements:Array, - context: Context, - options?: ContainerOptions, - ) { - super(); - this.#containerId = uuid(); - this.#metaData = { - ...metaData, - clientJSON: { - ...metaData?.clientJSON, - config: { - ...metaData?.clientJSON?.config, - options: { - ...metaData?.clientJSON?.config?.options, - ...options, - }, - }, - }, - }; - this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; - this.#skyflowElements = skyflowElements; - this.#context = context; - this.#options = options; - this.#eventEmitter = new EventEmitter(); - - this.#clientDomain = this.#metaData.clientDomain || ''; - const iframe = iframer({ - name: `${COLLECT_FRAME_CONTROLLER}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`, - referrer: this.#clientDomain, - }); - setAttributes(iframe, { - src: getIframeSrc(), - }); - setStyles(iframe, { ...CONTROLLER_STYLES }); - printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#containerMounted = true; - window.addEventListener('message', (event) => { - if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED - + this.#containerId) { - this.#isComposableFrameReady = true; - } - }); - } - - create = (input: RevealElementInput, options?: IRevealElementOptions) => { - const elementId = uuid(); - validateInputFormatOptions(options); - - const elementName = `${COMPOSABLE_REVEAL}:${btoa(elementId)}`; - this.#elementsList?.push({ - name: elementName, - ...input, - elementName, - elementId, - ...formatRevealElementOptions(options ?? {}), - }); - const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements ?? {})}:${this.#containerId}:${this.#context?.logLevel}:${btoa(this.#clientDomain ?? '')}`; - return new ComposableRevealElement(elementName, - this.#eventEmitter, - controllerIframeName); - }; - - setError(errors: Partial>) { - this.#customErrorMessages = errors; - // eslint-disable-next-line no-underscore-dangle - this.#eventEmitter._emit(`${CUSTOM_ERROR_MESSAGES}:${this.#containerId}`, { - errorMessages: this.#customErrorMessages, - }); - } - - #createMultipleElement = ( - multipleElements: ComposableElementGroup, - isSingleElementAPI: boolean = false, - ): ComposableContainer => { - try { - const elements: any[] = []; - this.#tempElements = deepClone(multipleElements); - this.#tempElements?.rows?.forEach((row) => { - row?.elements?.forEach((element) => { - const options = element ?? {}; - const { elementType } = options; - options.isMounted = false; - - options.label = element?.label; - options.skyflowID = element?.skyflowID; - - elements.push(options); - }); - }); - - this.#tempElements.elementName = isSingleElementAPI - ? elements[0].elementName - : `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}`; - if ( - isSingleElementAPI - && !this.#elements[elements[0].elementName] - && this.#hasElementName(elements[0].name) - ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); - } - - let element = this.#elements[this.#tempElements.elementName]; - if (element) { - if (isSingleElementAPI) { - // element.update(elements[0]); - } else { - // element.update(this.#tempElements); - } - } else { - const elementId = uuid(); - try { - element = new ComposableRevealInternalElement( - elementId, - this.#tempElements, - this.#metaData, - { - containerId: this.#containerId, - isMounted: this.#containerMounted, - type: this.type, - eventEmitter: this.#eventEmitter, - }, - this.#context, - ); - this.#elements[this.#tempElements.elementName] = element; - this.#skyflowElements[elementId] = element; - } catch (error: any) { - printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, - MessageType.ERROR, - this.#context.logLevel); - throw error; - } - } - this.#iframeID = element.iframeName(); - return element; - } catch (error: any) { - printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, - MessageType.ERROR, - this.#context.logLevel); - throw error; - } - }; - - #hasElementName = (name: string) => { - const tempElements = Object.keys(this.#elements); - for (let i = 0; i < tempElements.length; i += 1) { - if (atob(tempElements[i].split(':')[2]) === name) { - return true; - } - } - return false; - }; - - mount = (domElement: HTMLElement | string) => { - if (!domElement) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, - ['RevealElement'], true); - } - - const { layout } = this.#options; - if (sum(layout) !== this.#elementsList.length) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, [], true); - } - let count = 0; - layout.forEach((rowCount, index) => { - this.#elementGroup.rows = [ - ...this.#elementGroup.rows, - { elements: [] }, - ]; - for (let i = 0; i < rowCount; i++) { - this.#elementGroup.rows[index].elements.push( - this.#elementsList[count], - ); - count++; - } - }); - if (this.#options.styles) { - this.#elementGroup.styles = { - ...this.#options.styles, - }; - } - if (this.#options.errorTextStyles) { - this.#elementGroup.errorTextStyles = { - ...this.#options.errorTextStyles, - }; - } - if (this.#containerMounted) { - this.#containerElement = this.#createMultipleElement(this.#elementGroup, false); - this.#containerElement.mount(domElement); - this.#isMounted = true; - } - if (domElement instanceof HTMLElement - && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { - this.#shadowRoot = domElement.getRootNode() as ShadowRoot; - } else if (typeof domElement === 'string') { - const element = document.getElementById(domElement); - if (element && element.getRootNode() instanceof ShadowRoot) { - this.#shadowRoot = element.getRootNode() as ShadowRoot; - } - } - if (this.#shadowRoot !== null) { - this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { - this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); - }); - this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); - } - }; - - unmount = () => { - this.#containerElement.unmount(); - }; - - #emitEvent = (eventName: string, options?: Record, callback?: any) => { - const option = { - ...options, - errorMessages: this.#customErrorMessages, - }; - if (this.#shadowRoot) { - const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; - if (iframe?.contentWindow) { - iframe.contentWindow.postMessage({ - name: eventName, - ...option, - }, properties.IFRAME_SECURE_ORIGIN); - } - } else { - const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; - if (iframe?.contentWindow) { - iframe.contentWindow.postMessage({ - name: eventName, - ...option, - }, properties.IFRAME_SECURE_ORIGIN); - } - } - }; - - reveal(): Promise { - this.#revealRecords = []; - if (this.#isComposableFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#elementsList.forEach((currentElement) => { - // if (currentElement.isClientSetError()) { - // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); - // } - if (!currentElement.skyflowID) { - this.#revealRecords.push(currentElement); - } - }); - validateRevealElementRecords(this.#revealRecords); - const elementIds:{ frameId:string, token:string }[] = []; - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: element.name, - token: element.token, - }); - }); - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#emitEvent( - ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, - { - data: { - type: REVEAL_TYPES.REVEAL, - containerId: this.#containerId, - elementIds, - }, - clientConfig: { - vaultURL: this.#metaData?.clientJSON?.config?.vaultURL, - vaultID: this.#metaData?.clientJSON?.config?.vaultID, - authToken, - }, - context: this.#context, - }, - ); - - window?.addEventListener('message', (event) => { - if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (event?.data?.type - === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { - const revealData = event?.data?.data; - if (revealData?.errors) { - printLog( - parameterizedString(logs?.errorLogs?.FAILED_REVEAL), - MessageType.ERROR, - this.#context?.logLevel, - ); - reject(revealData); - } else { - printLog( - parameterizedString(logs?.infoLogs?.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context?.logLevel, - ); - resolve(revealData); - } - } - } - }); - }).catch((err:any) => { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - }); - } catch (err: any) { - printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - this.#elementsList.forEach((currentElement) => { - // if (currentElement.isClientSetError()) { - // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); - // } - if (!currentElement.skyflowID) { - this.#revealRecords.push(currentElement); - } - }); - validateRevealElementRecords(this.#revealRecords); - const elementIds:{ frameId:string, token:string }[] = []; - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: element.name, - token: element.token, - }); - }); - this.#getSkyflowBearerToken()?.then((authToken) => { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - window.addEventListener('message', (messagEevent) => { - if (messagEevent?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (messagEevent?.data?.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED - + this.#containerId) { - this.#emitEvent( - ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, { - data: { - type: REVEAL_TYPES.REVEAL, - containerId: this.#containerId, - elementIds, - }, - clientConfig: { - vaultURL: this.#metaData.clientJSON.config.vaultURL, - vaultID: this.#metaData.clientJSON.config.vaultID, - authToken, - }, - context: this.#context, - }, - ); - window.addEventListener('message', (event) => { - if (event?.origin === properties.IFRAME_SECURE_ORIGIN) { - if (event?.data?.type - === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { - const revealData = event?.data?.data; - if (revealData?.errors) { - printLog(parameterizedString(logs.errorLogs.FAILED_REVEAL), - MessageType.ERROR, this.#context.logLevel); - reject(revealData); - } else { - printLog( - parameterizedString(logs.infoLogs.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel, - ); - resolve(revealData); - } - } - } - }); - } - } - }); - }).catch((err:any) => { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - }); - } catch (err: any) { - printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); - } - }); - } -} -export default ComposableRevealContainer; diff --git a/src/core/external/reveal/composable-reveal-element.ts b/src/core/external/reveal/composable-reveal-element.ts deleted file mode 100644 index f023d420e..000000000 --- a/src/core/external/reveal/composable-reveal-element.ts +++ /dev/null @@ -1,66 +0,0 @@ -import EventEmitter from '../../../event-emitter'; -import { ContainerType } from '../../../skyflow'; -import { EventName, RenderFileResponse } from '../../../utils/common'; -import { ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_OPTIONS_TYPES } from '../../constants'; -import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; - -class ComposableRevealElement { - #elementName: string; - - #eventEmitter: EventEmitter; - - #iframeName: string; - - type: string = ContainerType.COMPOSABLE; - - #isMounted: boolean = false; - - constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { - this.#elementName = name; - this.#iframeName = iframeName; - this.#eventEmitter = eventEmitter; - this.#eventEmitter?.on?.(`${EventName.READY}:${this.#elementName}`, () => { - this.#isMounted = true; - }); - } - - iframeName(): string { - return this.#iframeName ?? ''; - } - - getID(): string { - return this.#elementName ?? ''; - } - - renderFile(): Promise { - return new Promise((resolve, reject) => { - // eslint-disable-next-line no-underscore-dangle - this.#eventEmitter?._emit?.( - `${ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST}:${this.#elementName}`, - {}, - (response) => { - if (response?.errors) { - reject(response); - } else if (response?.error) { - reject({ errors: response?.error }); - } else { - resolve(response); - } - }, - ); - }); - } - - update = (options: IRevealElementInput | IRevealElementOptions) => { - // eslint-disable-next-line no-underscore-dangle - this.#eventEmitter?._emit?.( - `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${this.#elementName}`, - { - options: options as IRevealElementInput | IRevealElementOptions, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, - }, - ); - }; -} - -export default ComposableRevealElement; diff --git a/src/core/external/reveal/reveal-element.ts b/src/core/external/reveal/reveal-element.ts deleted file mode 100644 index d926c800e..000000000 --- a/src/core/external/reveal/reveal-element.ts +++ /dev/null @@ -1,486 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -import bus from 'framebus'; -import SkyflowError from '../../../libs/skyflow-error'; -import uuid from '../../../libs/uuid'; -import { - Context, ErrorType, MessageType, RenderFileResponse, -} from '../../../utils/common'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import { - // eslint-disable-next-line max-len - FRAME_REVEAL, - ELEMENT_EVENTS_TO_IFRAME, - ELEMENT_EVENTS_TO_CONTAINER, - REVEAL_ELEMENT_OPTIONS_TYPES, - METRIC_TYPES, - ELEMENT_EVENTS_TO_CLIENT, - ELEMENT_TYPES, - EVENT_TYPES, - REVEAL_TYPES, - CUSTOM_ERROR_MESSAGES, -} from '../../constants'; -import IFrame from '../common/iframe'; -import SkyflowElement from '../common/skyflow-element'; -import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; -import { formatRevealElementOptions } from '../../../utils/helpers'; -import { - initalizeMetricObject, - pushElementEventWithTimeout, - updateMetricObjectValue, -} from '../../../metrics'; -import logs from '../../../utils/logs'; -import { parameterizedString, printLog } from '../../../utils/logs-helper'; -import { formatForRenderClient } from '../../../core-utils/reveal'; -import properties from '../../../properties'; -import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; -import { Metadata, RevealContainerProps } from '../../internal/internal-types'; -import EventEmitter from '../../../event-emitter'; - -const CLASS_NAME = 'RevealElement'; - -class RevealElement extends SkyflowElement { - #iframe: IFrame; - - #metaData: Metadata; - - #recordData: any; - - #containerId: string; - - #isMounted:boolean = false; - - #isClientSetError:boolean = false; - - #context: Context; - - #elementId: string; - - #readyToMount: boolean = false; - - #eventEmitter: EventEmitter; - - #isFrameReady: boolean; - - #domSelecter: string; - - #clientId: string; - - #isSkyflowFrameReady: boolean = false; - - #customerErrorMessages: Partial> = {}; - - constructor( - record: IRevealElementInput, - options: IRevealElementOptions = {}, - metaData: Metadata, - container: RevealContainerProps, - elementId: string, - context: Context, - ) { - super(); - this.#elementId = elementId; - this.#metaData = metaData; - this.#clientId = this.#metaData.uuid; - this.#recordData = { - ...record, - ...formatRevealElementOptions(options), - }; - this.#containerId = container.containerId; - this.#readyToMount = container.isMounted; - this.#eventEmitter = container.eventEmitter; - this.#context = context; - initalizeMetricObject(metaData, elementId); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.ELEMENT_TYPE_KEY, ELEMENT_TYPES.REVEAL); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.CONTAINER_NAME, ELEMENT_TYPES.REVEAL); - this.#iframe = new IFrame( - `${FRAME_REVEAL}:${btoa(uuid())}`, - metaData, - this.#containerId, - this.#context.logLevel, - ); - this.#domSelecter = ''; - this.#isFrameReady = false; - this.#readyToMount = true; - this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; - bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, (data) => { - this.#iframe.setIframeHeight(data.height); - }); - this.#eventEmitter.on(`${CUSTOM_ERROR_MESSAGES}:${this.#containerId}`, (data) => { - if (data?.errorMessages) { - this.#customerErrorMessages = data.errorMessages as Record; - } - }); - } - - getID() { - return this.#elementId; - } - - mount(domElementSelector: HTMLElement | string) { - if (!domElementSelector) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); - } - updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); - if ( - this.#metaData?.clientJSON?.config?.options?.trackMetrics - && this.#metaData.clientJSON.config?.options?.trackingKey - ) { - pushElementEventWithTimeout(this.#elementId); - } - - this.#readyToMount = true; - if (this.#readyToMount) { - this.#iframe.mount(domElementSelector, undefined, { - record: JSON.stringify({ - ...this.#metaData, - record: this.#recordData, - context: this.#context, - containerId: this.#containerId, - }), - }); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - if (this.#recordData.skyflowID) { - bus - // .target(location.origin) - .emit( - ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, - { - skyflowID: this.#recordData.skyflowID, - containerId: this.#containerId, - }, - ); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); - } else { - bus - // .target(location.origin) - .emit( - ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, - { - id: this.#recordData.token, - containerId: this.#containerId, - }, - ); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); - } - if (Object.prototype.hasOwnProperty.call(this.#recordData, 'skyflowID')) { - bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); - } - }); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.READY); - updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_START_TIME, Date.now()); - } - } - - renderFile(): Promise { - this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; - let altText = ''; - if (Object.prototype.hasOwnProperty.call(this.#recordData, 'altText')) { - altText = this.#recordData.altText; - } - this.setAltText('loading...'); - const loglevel = this.#context.logLevel; - if (this.#isSkyflowFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), - MessageType.LOG, - loglevel); - validateRenderElementRecord(this.#recordData); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#metaData.uuid, - { - type: REVEAL_TYPES.RENDER_FILE, - records: this.#recordData, - containerId: this.#containerId, - iframeName: this.#iframe.name, - errorMessages: this.#customerErrorMessages, - }, - (revealData: any) => { - if (revealData.errors) { - printLog(parameterizedString( - logs.errorLogs.FAILED_RENDER, - ), MessageType.ERROR, - this.#context.logLevel); - if (Object.prototype.hasOwnProperty.call(this.#recordData, 'altText')) { - this.setAltText(altText); - } - reject(formatForRenderClient(revealData, this.#recordData.column as string)); - } else { - printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, - CLASS_NAME, this.#recordData.skyflowID), - MessageType.LOG, this.#context.logLevel); - resolve(formatForRenderClient(revealData, this.#recordData.column as string)); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), - MessageType.LOG, loglevel); - } catch (err: any) { - printLog(`Error: ${err.message}`, MessageType.ERROR, - loglevel); - reject(err); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), - MessageType.LOG, - loglevel); - validateRenderElementRecord(this.#recordData); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#metaData.uuid, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#metaData.uuid, - { - type: REVEAL_TYPES.RENDER_FILE, - records: this.#recordData, - containerId: this.#containerId, - iframeName: this.#iframe.name, - errorMessages: this.#customerErrorMessages, - }, - (revealData: any) => { - if (revealData.errors) { - printLog(parameterizedString( - logs.errorLogs.FAILED_RENDER, - ), MessageType.ERROR, - this.#context.logLevel); - if (Object.prototype.hasOwnProperty.call(this.#recordData, 'altText')) { - this.setAltText(altText); - } - reject(formatForRenderClient(revealData, this.#recordData.column as string)); - } else { - printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, - CLASS_NAME, this.#recordData.skyflowID), - MessageType.LOG, this.#context.logLevel); - resolve(formatForRenderClient(revealData, this.#recordData.column as string)); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), - MessageType.LOG, loglevel); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), - MessageType.LOG, loglevel); - } catch (err: any) { - printLog(`Error: ${err.message}`, MessageType.ERROR, - loglevel); - reject(err); - } - }); - } - - iframeName(): string { - return this.#iframe.name; - } - - isMounted():boolean { - return this.#isMounted; - } - - hasToken():boolean { - if (this.#recordData.token) return true; - return false; - } - - isClientSetError():boolean { - return this.#isClientSetError; - } - - getRecordData() { - return this.#recordData; - } - - setErrorOverride(clientErrorText: string) { - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { - name: this.#iframe.name, - isTriggerError: true, - clientErrorText, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { - name: this.#iframe.name, - isTriggerError: true, - clientErrorText, - }); - }); - } - this.#isClientSetError = true; - } - - setError(clientErrorText:string) { - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { - name: this.#iframe.name, - isTriggerError: true, - clientErrorText, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { - name: this.#iframe.name, - isTriggerError: true, - clientErrorText, - }); - }); - } - this.#isClientSetError = true; - } - - resetError() { - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { - name: this.#iframe.name, - isTriggerError: false, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { - name: this.#iframe.name, - isTriggerError: false, - }); - }); - } - this.#isClientSetError = false; - } - - setAltText(altText:string) { - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, - updatedValue: altText, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, - updatedValue: altText, - }); - }); - } - } - - clearAltText() { - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, - updatedValue: null, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, - updatedValue: null, - }); - }); - } - } - - setToken(token:string) { - this.#recordData = { - ...this.#recordData, - token, - }; - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, - updatedValue: token, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, - updatedValue: token, - }); - }); - } - } - - unmount() { - if (this.#recordData.skyflowID) { - this.#isMounted = false; - this.#iframe.container?.remove(); - } - this.#isMounted = false; - this.#iframe.unmount(); - } - - update(options: IRevealElementInput) { - this.#recordData = { - ...this.#recordData, - ...options, - }; - - if (this.#isMounted) { - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, - updatedValue: options, - }); - } else { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { - this.#isMounted = true; - bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { - name: this.#iframe.name, - updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, - updatedValue: options, - }); - }); - } - } -} - -export default RevealElement; diff --git a/src/core/external/skyflow-container.ts b/src/core/external/skyflow-container.ts deleted file mode 100644 index 3604ffdfe..000000000 --- a/src/core/external/skyflow-container.ts +++ /dev/null @@ -1,542 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -import bus from 'framebus'; -import Client from '../../client'; -import iframer, { - getIframeSrc, - setAttributes, - setStyles, -} from '../../iframe-libs/iframer'; -import properties from '../../properties'; -import { - validateInsertRecords, - validateDetokenizeInput, - validateInitConfig, - validateGetInput, - validateGetByIdInput, - validateUpsertOptions, - validateDeleteRecords, - validateUpdateRecord, -} from '../../utils/validators'; -import { - CONTROLLER_STYLES, - ELEMENT_EVENTS_TO_IFRAME, - SKYFLOW_FRAME_CONTROLLER, - PUREJS_TYPES, -} from '../constants'; -import { - printLog, - parameterizedString, -} from '../../utils/logs-helper'; -import logs from '../../utils/logs'; -import { - IDetokenizeInput, - IGetInput, - Context, - MessageType, - IGetByIdInput, - IInsertOptions, - IDeleteOptions, - IDeleteRecordInput, - IGetOptions, - InsertResponse, - GetByIdResponse, - GetResponse, - DeleteResponse, - IInsertRecordInput, - DetokenizeResponse, - IUpdateRequest, - UpdateResponse, - IUpdateOptions, -} from '../../utils/common'; - -const CLASS_NAME = 'SkyflowContainer'; -class SkyflowContainer { - #containerId: string; - - #client: Client; - - isControllerFrameReady: boolean = false; - - #context: Context; - - constructor(client: Client, context: Context) { - this.#client = client; - this.#containerId = this.#client.toJSON()?.metaData?.uuid || ''; - this.#context = context; - const clientDomain = window.location.origin || ''; - const iframe = iframer({ - name: `${SKYFLOW_FRAME_CONTROLLER}:${this.#containerId}:${btoa(clientDomain)}:${!!this.#client.toJSON()?.config?.options?.trackingKey}`, - referrer: clientDomain, - }); - setAttributes(iframe, { - src: getIframeSrc(), - }); - setStyles(iframe, { ...CONTROLLER_STYLES }); - document.body.append(iframe); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, (data, callback) => { - printLog(parameterizedString(logs.infoLogs.CAPTURE_PUREJS_FRAME, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - callback({ - client: this.#client, - context, - }); - this.isControllerFrameReady = true; - }); - printLog(parameterizedString(logs.infoLogs.PUREJS_CONTROLLER_INITIALIZED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - } - - detokenize(detokenizeInput: IDetokenizeInput): Promise { - if (this.isControllerFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_DETOKENIZE_INPUT, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - validateDetokenizeInput(detokenizeInput); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.DETOKENIZE, - records: detokenizeInput.records, - }, - (revealData: any) => { - if (revealData.error) reject(revealData.error); - else resolve(revealData); - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.DETOKENIZE), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - reject(e); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_DETOKENIZE_INPUT, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - validateDetokenizeInput(detokenizeInput); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.DETOKENIZE, - records: detokenizeInput.records, - }, - (revealData: any) => { - if (revealData.error) reject(revealData.error); - else resolve(revealData); - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.DETOKENIZE), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - reject(e); - } - }); - } - - insert(records: IInsertRecordInput, options?:IInsertOptions): Promise { - if (this.isControllerFrameReady) { - return new Promise((resolve, reject) => { - validateInitConfig(this.#client.config); - try { - printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - if (options) { - options = { ...options, tokens: options?.tokens !== undefined ? options.tokens : true }; - } else { - options = { - tokens: true, - }; - } - if (options?.upsert) { - validateUpsertOptions(options.upsert); - } - validateInsertRecords(records, options); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.INSERT, - records, - options, - }, - (insertedData: any) => { - if (insertedData.error) { - printLog(`${JSON.stringify(insertedData.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(insertedData.error); - } else resolve(insertedData); - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.INSERT), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - - reject(e); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - - if (options) { - options = { ...options, tokens: options?.tokens !== undefined ? options.tokens : true }; - } else { - options = { - tokens: true, - }; - } - if (options?.upsert) { - validateUpsertOptions(options.upsert); - } - validateInsertRecords(records, options); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.INSERT, - records, - options, - }, - (insertedData: any) => { - if (insertedData.error) { - printLog(`${JSON.stringify(insertedData.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(insertedData.error); - } else resolve(insertedData); - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.INSERT), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - reject(e); - } - }); - } - - update(record: IUpdateRequest, options?: IUpdateOptions): Promise { - if (this.isControllerFrameReady) { - return new Promise((resolve, reject) => { - validateInitConfig(this.#client.config); - try { - printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - - validateUpdateRecord(record, options); - - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.UPDATE, - record, - options, - }, - (updatedData: any) => { - if (updatedData.error) { - printLog(`${JSON.stringify(updatedData.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(updatedData.error); - } else resolve(updatedData); - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.UPDATE), - MessageType.LOG, this.#context.logLevel); - } catch (e: any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - reject(e); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - - validateUpdateRecord(record, options); - - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.UPDATE, - record, - options, - }, - (updatedData: any) => { - if (updatedData.error) { - printLog(`${JSON.stringify(updatedData.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(updatedData.error); - } else resolve(updatedData); - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.UPDATE), - MessageType.LOG, this.#context.logLevel); - } catch (e: any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - reject(e); - } - }); - } - - getById(getByIdInput: IGetByIdInput): Promise { - if (this.isControllerFrameReady) { - return new Promise((resolve, reject) => { - validateInitConfig(this.#client.config); - try { - printLog(parameterizedString(logs.infoLogs.VALIDATE_GET_BY_ID_INPUT, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - validateGetByIdInput(getByIdInput); - - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.GET_BY_SKYFLOWID, - records: getByIdInput.records, - }, - (revealData: any) => { - if (revealData.error) reject(revealData.error); - else resolve(revealData); - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, - CLASS_NAME, PUREJS_TYPES.GET_BY_SKYFLOWID), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - - reject(e); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_GET_BY_ID_INPUT, - CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - - validateGetByIdInput(getByIdInput); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.GET_BY_SKYFLOWID, - records: getByIdInput.records, - }, - (revealData: any) => { - if (revealData.error) reject(revealData.error); - else resolve(revealData); - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, - CLASS_NAME, PUREJS_TYPES.GET_BY_SKYFLOWID), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - - reject(e); - } - }); - } - - get(getInput: IGetInput, options?: IGetOptions): Promise { - if (this.isControllerFrameReady) { - return new Promise((resolve, reject) => { - validateInitConfig(this.#client.config); - try { - printLog(parameterizedString(logs.infoLogs.VALIDATE_GET_INPUT, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - validateGetInput(getInput, options); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.GET, - records: getInput.records, - options, - }, - (revealData: any) => { - if (revealData.error) reject(revealData.error); - else resolve(revealData); - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, - CLASS_NAME, PUREJS_TYPES.GET), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - - reject(e); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_GET_INPUT, - CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - - validateGetInput(getInput, options); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.GET, - records: getInput.records, - options, - }, - (revealData: any) => { - if (revealData.error) reject(revealData.error); - else resolve(revealData); - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, - CLASS_NAME, PUREJS_TYPES.GET), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - - reject(e); - } - }); - } - - delete(records: IDeleteRecordInput, options?: IDeleteOptions): Promise { - if (this.isControllerFrameReady) { - return new Promise((resolve, reject) => { - validateInitConfig(this.#client.config); - try { - printLog( - parameterizedString(logs.infoLogs.VALIDATE_DELETE_INPUT, CLASS_NAME), MessageType.LOG, - this.#context.logLevel, - ); - - validateDeleteRecords(records, options); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.DELETE, - records, - options, - }, - (deletedData: any) => { - if (deletedData.error) { - printLog(`${JSON.stringify(deletedData.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(deletedData.error); - } else { - resolve(deletedData); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.DELETE), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - - reject(e); - } - }); - } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#client.config); - printLog(parameterizedString(logs.infoLogs.VALIDATE_RECORDS, CLASS_NAME), MessageType.LOG, - this.#context.logLevel); - - validateDeleteRecords(records, options); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#containerId, () => { - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#containerId, - { - type: PUREJS_TYPES.DELETE, - records, - options, - }, - (deletedData: any) => { - if (deletedData.error) { - printLog(`${JSON.stringify(deletedData.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(deletedData.error); - } else resolve(deletedData); - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_PURE_JS_REQUEST, CLASS_NAME, - PUREJS_TYPES.DELETE), - MessageType.LOG, this.#context.logLevel); - } catch (e:any) { - printLog(e.message, MessageType.ERROR, this.#context.logLevel); - reject(e); - } - }); - } -} -export default SkyflowContainer; diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts deleted file mode 100644 index d4c3543ff..000000000 --- a/src/core/internal/frame-element-init.ts +++ /dev/null @@ -1,922 +0,0 @@ -import injectStylesheet from 'inject-stylesheet'; -import bus from 'framebus'; -import get from 'lodash/get'; -import { getValueAndItsUnit, validateAndSetupGroupOptions } from '../../libs/element-options'; -import { getFlexGridStyles } from '../../libs/styles'; -import { ContainerType } from '../../skyflow'; -import { - Context, Env, ErrorType, LogLevel, - MessageType, -} from '../../utils/common'; -import { - fileValidation, generateUploadFileName, getContainerType, vaildateFileName, -} from '../../utils/helpers'; -import { - ALLOWED_MULTIPLE_FIELDS_STYLES, - COLLECT_TYPES, - ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, ERROR_TEXT_STYLES, STYLE_TYPE, -} from '../constants'; -import IFrameFormElement from './iframe-form'; -import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; -import FrameElement from '.'; -import { - checkForElementMatchRule, checkForValueMatch, constructElementsInsertReq, - constructInsertRecordRequest, insertDataInCollect, - updateRecordsBySkyflowIDComposable, -} from '../../core-utils/collect'; -import SkyflowError from '../../libs/skyflow-error'; -import SKYFLOW_ERROR_CODE from '../../utils/constants'; -import Client from '../../client'; -import { printLog } from '../../utils/logs-helper'; - -const set = require('set-value'); - -export default class FrameElementInit { - iframeFormElement: IFrameFormElement | undefined; - - clientMetaData: any; - - context: Context; - - #domForm: HTMLFormElement; - - frameElement!: FrameElement; - - private static frameEle?: any; - - containerId: string; - - group: any; - - frameList: FrameElement[] = []; - - iframeFormList: IFrameFormElement[] = []; - - #client!: Client; - - constructor() { - // this.createIframeElement(frameName, label, skyflowID, isRequired); - this.context = { logLevel: LogLevel.INFO, env: Env.PROD }; // client level - this.containerId = ''; - this.#domForm = document.createElement('form'); - this.#domForm.action = '#'; - this.#domForm.onsubmit = (event) => { - event.preventDefault(); - }; - this.updateGroupData(); - this.createContainerDiv(this.group); - bus - .target(this.clientMetaData?.clientDomain) - .emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { - data.client.config = { - ...data.client.config, - }; - this.#client = Client.fromJSON(data.client) as any; - }); - - window.addEventListener('message', this.handleCollectCall); - } - - private handleCollectCall = (event: MessageEvent) => { - if (event?.origin === this.clientMetaData?.clientDomain) { - this.iframeFormList.forEach((inputElement) => { - if (inputElement) { - if (inputElement.fieldType - === ELEMENTS.MULTI_FILE_INPUT.name) { - if (event?.data && event?.data?.name === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${inputElement.iFrameName}`) { - this.#client = Client.fromJSON(event?.data?.clientConfig); - this.multipleUploadFiles(inputElement, event?.data?.clientConfig, - event?.data?.options, event?.data?.errorMessages) - ?.then((response: any) => { - window?.parent.postMessage({ - type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, - data: response, - }, this.clientMetaData?.clientDomain); - }).catch((error) => { - window?.parent.postMessage({ - type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, - data: error, - }, this.clientMetaData?.clientDomain); - }); - } - } - } - }); - - if (event?.data && event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS - + this.containerId) { - if (event?.data?.data && event?.data?.data?.type === COLLECT_TYPES.COLLECT) { - this.tokenize(event?.data?.data, event?.data?.clientConfig, event?.data?.errorMessages) - .then((response: any) => { - window?.parent.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, - data: response, - }, this.clientMetaData?.clientDomain); - }) - .catch((error) => { - window?.parent.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, - data: error, - }, this.clientMetaData?.clientDomain); - }); - } else if (event.data.data && event.data.data.type === COLLECT_TYPES.FILE_UPLOAD) { - this.parallelUploadFiles(event.data.data, - event.data.clientConfig, event?.data?.errorMessages) - .then((response: any) => { - window?.parent.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, - data: response, - }, this.clientMetaData?.clientDomain); - }) - .catch((error) => { - window?.parent.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, - data: error, - }, this.clientMetaData?.clientDomain); - }); - } - } - if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId) { - const data = event.data; - data.client.config = { - ...data.client.config, - }; - this.#client = Client.fromJSON(data.client) as any; - } - } - }; - - private parallelUploadFiles = (options, config, - errorMessages?: Record) => new Promise((rootResolve, rootReject) => { - const promises: Promise[] = []; - this.iframeFormList.forEach((inputElement) => { - let res: Promise; - if (inputElement) { - if ( - inputElement.fieldType - === ELEMENTS.FILE_INPUT.name - ) { - res = this.uploadFiles(inputElement, config, errorMessages); - promises.push(res); - } - } - }); - if (promises.length === 0) { - rootReject(new SkyflowError(SKYFLOW_ERROR_CODE.NO_FILE_ELEMENT_FOUND, [], true)); - } - Promise.allSettled( - promises, - ).then((resultSet) => { - const fileUploadResponse: any[] = []; - const errorResponse: any[] = []; - resultSet.forEach((result) => { - if (result.status === 'fulfilled') { - if (result.value !== undefined && result.value !== null) { - if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { - errorResponse.push(result.value); - } else { - const response = typeof result.value === 'string' - ? JSON.parse(result.value) - : result.value; - fileUploadResponse.push(response); - } - } - } else if (result.status === 'rejected') { - if (result.reason?.error) { - errorResponse.push({ error: result.reason?.error }); - } else { - errorResponse.push(result.reason); - } - } - }); - if (errorResponse.length === 0) { - rootResolve({ fileUploadResponse }); - } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); - else rootReject({ fileUploadResponse, errorResponse }); - }); - }); - - uploadFiles = (fileElement, clientConfig, errorMessages?: Record) => { - this.#client = new Client(clientConfig, { - uuid: '', - clientDomain: '', - }); - if (errorMessages && this.#client) { - this.#client.setErrorMessages(errorMessages); - } - if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const fileUploadObject: any = {}; - - const { - state, tableName, skyflowID, onFocusChange, preserveFileName, - } = fileElement; - - if (state.isRequired) { - onFocusChange(false); - } - try { - fileValidation(state.value, state.isRequired, fileElement); - } catch (err) { - return Promise.reject(err); - } - - const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); - - if (!validatedFileState) { - return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); - } - fileUploadObject[state.name] = state.value; - - const formData = new FormData(); - - const column = Object.keys(fileUploadObject)[0]; - - const value: Blob = Object.values(fileUploadObject)[0] as Blob; - - formData.append('columnName', column); - formData.append('tableName', tableName); - - if (preserveFileName) { - const isValidFileName = vaildateFileName(state.value.name); - if (!isValidFileName) { - return Promise.reject( - new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), - ); - } - formData.append('file', value); - } else { - const generatedFileName = generateUploadFileName(state.value.name); - formData.append('file', new File([value], generatedFileName, { type: state.value.type })); - } - - if (skyflowID) { - formData.append('skyflowID', skyflowID); - } - - const client = this.#client; - const sendRequest = () => new Promise((rootResolve, rootReject) => { - client - .request({ - body: formData, - requestMethod: 'POST', - url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, - headers: { - authorization: `Bearer ${clientConfig.authToken}`, - 'content-type': 'multipart/form-data', - }, - }) - .then((response: any) => { - rootResolve(response); - }) - .catch((error) => { - if (error?.error) { - rootReject({ - error: { - code: error?.error?.code, - description: error?.error?.description, - type: error?.error?.type, - }, - }); - } - rootReject(error); - }); - }); - - return new Promise((resolve, reject) => { - sendRequest() - .then((res) => resolve(res)) - .catch((err) => { - reject(err); - }); - }); - }; - - private tokenize = (options, clientConfig: any, errorMessages?: Record) => { - let errorMessage = ''; - const insertRequestObject: any = {}; - const updateRequestObject: any = {}; - - for (let i = 0; i < this.iframeFormList.length; i += 1) { - const inputElement = this.iframeFormList[i]; - if (inputElement) { - if ( - inputElement.fieldType - !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType - !== ELEMENTS.MULTI_FILE_INPUT.name - ) { - const { - // eslint-disable-next-line max-len - state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, - setValue, - } = inputElement; - if (state.isRequired || !state.isValid) { - onFocusChange(false); - } - if (validations - && checkForElementMatchRule(validations) - && checkForValueMatch(validations, inputElement)) { - setValue(state.value); - onFocusChange(false); - } - if (!state.isValid || !state.isComplete) { - if (doesClientHasError) { - errorMessage += `${state.name}:${clientErrorText}`; - } else { errorMessage += `${state.name}:${errorText} `; } - } - } - } - } - - // return for error - if (errorMessage.length > 0) { - // eslint-disable-next-line max-len - return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.COMPLETE_AND_VALID_INPUTS, [`${errorMessage}`], true)); - } - // eslint-disable-next-line consistent-return - for (let i = 0; i < this.iframeFormList.length; i += 1) { - const inputElement = this.iframeFormList[i]; - if (inputElement) { - const { - state, tableName, validations, skyflowID, - } = inputElement; - if (tableName) { - if ( - inputElement.fieldType - !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType - !== ELEMENTS.MULTI_FILE_INPUT.name - ) { - if ( - inputElement.fieldType - === ELEMENTS.checkbox.name - ) { - if (insertRequestObject[state.name]) { - insertRequestObject[state.name] = `${insertRequestObject[state.name]},${state.value - }`; - } else { - insertRequestObject[state.name] = state.value; - } - } else if (insertRequestObject[tableName] && !(skyflowID === '') && skyflowID === undefined) { - if (get(insertRequestObject[tableName], state.name) - && !(validations && checkForElementMatchRule(validations))) { - return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, - [state.name, tableName], true)); - } - set( - insertRequestObject[tableName], - state.name, - inputElement.getUnformattedValue(), - ); - } else if (skyflowID || skyflowID === '') { - if (skyflowID === '' || skyflowID === null) { - return Promise.reject(new SkyflowError( - SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_IN_ADDITIONAL_FIELDS, - )); - } - if (updateRequestObject[skyflowID]) { - set( - updateRequestObject[skyflowID], - state.name, - inputElement.getUnformattedValue(), - ); - } else { - updateRequestObject[skyflowID] = {}; - set( - updateRequestObject[skyflowID], - state.name, - inputElement.getUnformattedValue(), - ); - set( - updateRequestObject[skyflowID], - 'table', - tableName, - ); - } - } else { - insertRequestObject[tableName] = {}; - set( - insertRequestObject[tableName], - state.name, - inputElement.getUnformattedValue(), - ); - } - } - } - } - } - let finalInsertRequest; - let finalInsertRecords; - let finalUpdateRecords; - try { - [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( - insertRequestObject, updateRequestObject, options, - ); - finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options); - } catch (error:any) { - return Promise.reject({ - error: error?.message, - }); - } - this.#client = new Client(clientConfig, { - uuid: '', - clientDomain: '', - }); - const client = this.#client; - if (errorMessages && client) { - this.#client.setErrorMessages(errorMessages); - } - const sendRequest = () => new Promise((rootResolve, rootReject) => { - const insertPromiseSet: Promise[] = []; - - // const clientId = client.toJSON()?.metaData?.uuid || ''; - // getAccessToken(clientId).then((authToken) => { - if (finalInsertRequest.length !== 0) { - insertPromiseSet.push( - insertDataInCollect(finalInsertRequest, - client, options, finalInsertRecords, clientConfig.authToken as string), - ); - } - if (finalUpdateRecords.updateRecords.length !== 0) { - insertPromiseSet.push( - updateRecordsBySkyflowIDComposable( - finalUpdateRecords, client, options, clientConfig.authToken as string, - ), - ); - } - if (insertPromiseSet.length !== 0) { - Promise.allSettled(insertPromiseSet).then((resultSet: any) => { - const recordsResponse: any[] = []; - const errorsResponse: any[] = []; - - resultSet.forEach((result: - { status: string; value: any; reason?: any; }) => { - if (result.status === 'fulfilled') { - if (result.value.records !== undefined && Array.isArray(result.value.records)) { - result.value.records.forEach((record) => { - recordsResponse.push(record); - }); - } - if (result.value.errors !== undefined && Array.isArray(result.value.errors)) { - result.value.errors.forEach((error) => { - errorsResponse.push(error); - }); - } - } else { - if (result.reason?.records !== undefined && Array.isArray(result.reason?.records)) { - result.reason.records.forEach((record) => { - recordsResponse.push(record); - }); - } - if (result.reason?.errors !== undefined && Array.isArray(result.reason?.errors)) { - result.reason.errors.forEach((error) => { - errorsResponse.push(error); - }); - } - } - }); - if (errorsResponse.length === 0) { - rootResolve({ records: recordsResponse }); - } else if (recordsResponse.length === 0) rootReject({ errors: errorsResponse }); - else rootReject({ records: recordsResponse, errors: errorsResponse }); - }); - } - // }).catch((err) => { - // rootReject({ - // error: err, - // }); - // }); - }); - - return new Promise((resolve, reject) => { - sendRequest() - .then((res) => resolve(res)) - .catch((err) => reject(err)); - }); - }; - - // eslint-disable-next-line consistent-return - private multipleUploadFiles = - (fileElement: IFrameFormElement, - clientConfig, metaData, - errorMessages?: Record) => new Promise((rootResolve, rootReject) => { - this.#client = new Client(clientConfig, { - uuid: '', - clientDomain: '', - }); - if (errorMessages && this.#client) { - this.#client.setErrorMessages(errorMessages); - } - if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - - const { - state, tableName, onFocusChange, preserveFileName, - } = fileElement; - if (state.isRequired) { - onFocusChange(false); - } - - if (state.value === undefined || state.value === null || state.value === '') { - rootReject({ error: 'No files selected' }); - return; - } - - const files = state.value instanceof FileList ? Array.from(state.value) : [state.value]; - try { - this.validateFiles(files, state, fileElement); - } catch (err: any) { - rootReject({ errorResponse: [{ error: err?.error || err?.errors?.[0] || err }] }); - return; - } - - const uploadFile = (file: File, skyflowID?: string) => { - const formData = new FormData(); - formData.append('columnName', state.name); - if (tableName) formData.append('tableName', tableName); - if (preserveFileName) { - formData.append('file', file); - } else { - const generatedFileName = generateUploadFileName(file.name); - formData.append('file', new File([file], generatedFileName, { type: file.type })); - } - if (skyflowID) formData.append('skyflowID', skyflowID); - const client = this.#client; - return this.#client.request({ - body: formData, - requestMethod: 'POST', - url: `${client.config.vaultURL}/v2/vaults/${this.#client.config.vaultID}/files/upload`, - headers: { - authorization: `Bearer ${clientConfig.authToken}`, - 'content-type': 'multipart/form-data', - }, - }); - }; - - if (metaData && Object.keys(metaData).length > 0) { - const insertRequest = this.createInsertRequest(files.length, metaData); - this.insertDataCallInMultiFiles( - insertRequest, this.#client, tableName as string, clientConfig.authToken as string, - ).then((response: any) => { - const skyflowIDs = this.extractSkyflowIDs(response); - if (skyflowIDs.length === 0) { - rootReject({ error: 'No skyflow IDs returned from insert data' }); - return; - } - const promises = files.map((file, idx) => uploadFile(file, skyflowIDs[idx])); - Promise.allSettled(promises).then((resultSet) => { - const fileUploadResponse: any[] = []; - const errorResponse: any[] = []; - resultSet.forEach((result) => { - if (result.status === 'fulfilled') { - if (result.value !== undefined && result.value !== null) { - if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { - errorResponse.push(result.value); - } else { - const response1 = typeof result.value === 'string' - ? JSON.parse(result.value) - : result.value; - fileUploadResponse.push(response1); - } - } - } else if (result.status === 'rejected') { - if (result?.reason?.error) { - errorResponse.push({ error: result?.reason?.error }); - } else { - errorResponse.push({ error: result.reason }); - } - } - }); - if (errorResponse.length === 0) { - rootResolve({ fileUploadResponse }); - } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); - else rootReject({ fileUploadResponse, errorResponse }); - }); - }).catch((error) => { - printLog(`${error}`, MessageType.LOG, this.context?.logLevel); - rootReject({ - error: error?.error || error, - }); - }); - } else { - const promises = files.map((file) => uploadFile(file)); - Promise.allSettled(promises).then((resultSet) => { - const fileUploadResponse: any[] = []; - const errorResponse: any[] = []; - resultSet.forEach((result) => { - if (result.status === 'fulfilled') { - if (result.value !== undefined && result.value !== null) { - if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { - errorResponse.push(result.value); - } else { - const response1 = typeof result.value === 'string' - ? JSON.parse(result.value) - : result.value; - fileUploadResponse.push(response1); - } - } - } else if (result.status === 'rejected') { - if (result?.reason?.error) { - errorResponse.push({ error: result?.reason?.error }); - } else { - errorResponse.push({ error: result.reason }); - } - } - }); - if (errorResponse.length === 0) { - rootResolve({ fileUploadResponse }); - } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); - else rootReject({ fileUploadResponse, errorResponse }); - }); - } - }); - - private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { - if (files.length > fileElement.maxFileCount) { - throw new SkyflowError( - SKYFLOW_ERROR_CODE.FILE_COUNT_EXCEEDED, - [String(fileElement.maxFileCount)], - true, - ); - } - files.forEach((file) => { - const validatedFileState = fileValidation(file, state.isRequired, fileElement); - if (!validatedFileState) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); - } - - const isValidFileName = vaildateFileName(file.name); - if (!isValidFileName) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); - } - }); - return true; - }; - - private createInsertRequest = (numberOfRequests: number, options = {}) => { - // Create basic request structure - const request = { - records: [] as Array<{ fields: Record }>, - tokenization: false, - }; - - // Add empty field objects based on number of requests - for (let i = 0; i < numberOfRequests; i += 1) { - request.records.push({ - fields: options === undefined ? {} : options, - }); - } - - return request; - }; - - private extractSkyflowIDs = (response: { records: Array<{ skyflow_id: string }> }): string[] => { - if (!response?.records || !Array.isArray(response.records)) { - return []; - } - - return response.records - .map((record) => record.skyflow_id) - .filter((id) => id !== undefined && id !== null); - }; - - private insertDataCallInMultiFiles = ( - insertRequest, - client: Client, - tableName: string, - authToken: string, - ) => new Promise((rootResolve, rootReject) => { - client - .request({ - body: JSON.stringify(insertRequest), - requestMethod: 'POST', - url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}`, - headers: { - authorization: `Bearer ${authToken}`, - 'content-type': 'application/json', - }, - }) - .then((response: any) => { - // Extract skyflow IDs from response - const skyflowIDs = this.extractSkyflowIDs(response); - rootResolve({ - ...response, - skyflowIDs, // Add extracted IDs to response - }); - }) - .catch((error) => { - if (error?.error) { - rootReject({ - error: { - code: error?.error?.code, - description: error?.error?.description, - type: error?.error?.type, - }, - }); - } else { - rootReject(error); - } - }); - }); - - updateGroupData = () => { - const frameName = window.name; - const url = window.location?.href; - const configIndex = url.indexOf('?'); - const encodedString = configIndex !== -1 ? decodeURIComponent(url.substring(configIndex + 1)) : ''; - const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; - this.clientMetaData = parsedRecord.metaData; - this.context = { - logLevel: this.clientMetaData?.clientJSON?.config?.options?.logLevel || LogLevel.ERROR, - env: this.clientMetaData?.clientJSON?.config?.options?.env || Env.PROD, - }; - this.group = parsedRecord.record; - this.containerId = parsedRecord.containerId; - bus - .target(this.clientMetaData?.clientDomain) - .on(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + frameName, (data) => { - if (data.name === frameName) { - if (data.options !== undefined) { - this.createContainerDiv(data.options); - } - } - }); - }; - - createIframeElement = (frameName, label, skyflowID, isRequired) => { - this.iframeFormElement = new IFrameFormElement(frameName, label, { - ...this.clientMetaData, - isRequired, - }, this.context, skyflowID); - this.iframeFormList.push(this.iframeFormElement); - return this.iframeFormElement; - }; - - static startFrameElement = () => { - FrameElementInit.frameEle = new FrameElementInit(); - }; - - createContainerDiv = (newGroup) => { - this.group = validateAndSetupGroupOptions( - this.group, - newGroup, - false, - ); - this.group = newGroup; - const { - rows, styles, errorTextStyles, - } = this.group; - const isComposableContainer = getContainerType(window.name) === ContainerType.COMPOSABLE; - this.group.spacing = getValueAndItsUnit(this.group.spacing).join(''); - const rootDiv = document.createElement('div'); - rootDiv.className = 'container'; - const containerStylesByClassName = getFlexGridStyles({ - 'align-items': this.group.alignItems || 'stretch', - 'justify-content': this.group.justifyContent || 'flex-start', - spacing: this.group.spacing, - }); - - injectStylesheet.injectWithAllowlist( - { - [`.${rootDiv.className}`]: containerStylesByClassName, - }, - ALLOWED_MULTIPLE_FIELDS_STYLES, - ); - let count = 0; - rows.forEach((row, rowIndex) => { - row.spacing = getValueAndItsUnit(row.spacing).join(''); - const rowDiv = document.createElement('div'); - rowDiv.id = `row-${rowIndex}`; - - const intialRowStyles = { - 'align-items': row.alignItems || 'stretch', - 'justify-content': row.justifyContent || 'flex-start', - spacing: row.spacing, - padding: this.group.spacing, - }; - const rowStylesByClassName = getFlexGridStyles(intialRowStyles); - let errorTextElement; - if (isComposableContainer) { - rowDiv.className = `${rowDiv.id} SkyflowElement-${rowDiv.id}-base`; - const rowStyles = { - [STYLE_TYPE.BASE]: { - // ...rowStylesByClassName, - // alignItems: rowStylesByClassName['align-items'], - // justifyContent: rowStylesByClassName['justify-content'], - ...(styles && styles[STYLE_TYPE.BASE]), - }, - }; - - getCssClassesFromJss(rowStyles, `${rowDiv.id}`); - - errorTextElement = document.createElement('span'); - errorTextElement.id = `${rowDiv.id}-error`; - errorTextElement.className = 'SkyflowElement-row-error-base'; - - const errorStyles = { - [STYLE_TYPE.BASE]: { - ...ERROR_TEXT_STYLES, - ...(errorTextStyles && errorTextStyles[STYLE_TYPE.BASE]), - }, - }; - getCssClassesFromJss(errorStyles, 'row-error'); - if (errorTextStyles && errorTextStyles[STYLE_TYPE.GLOBAL]) { - generateCssWithoutClass(errorTextStyles[STYLE_TYPE.GLOBAL]); - } - } else { - rowDiv.className = `row-${rowIndex}`; - injectStylesheet.injectWithAllowlist( - { - [`.${rowDiv.className}`]: rowStylesByClassName, - }, - ALLOWED_MULTIPLE_FIELDS_STYLES, - ); - } - - const errorTextMap = {}; - row.elements.forEach((element) => { - const elementDiv = document.createElement('div'); - elementDiv.className = `element-${count}`; - elementDiv.id = `${rowDiv.id}:element-${count}`; - count += 1; - const elementStylesByClassName = { - padding: row.spacing, - }; - injectStylesheet.injectWithAllowlist( - { - [`.${elementDiv.className}`]: elementStylesByClassName, - }, - ALLOWED_MULTIPLE_FIELDS_STYLES, - ); - // create a iframeelement - // create element by passing iframeformelement and options and mount by default returns - const iFrameFormElement = this.createIframeElement( - element.elementName, - element.label, - element.skyflowID, - element.required, - ); - this.frameElement = new FrameElement( - iFrameFormElement, - element, - elementDiv, - this.clientMetaData.clientDomain, - ); - this.frameList.push(this.frameElement); - - if (isComposableContainer && errorTextElement) { - iFrameFormElement.on(ELEMENT_EVENTS_TO_CLIENT.BLUR, (state) => { - errorTextMap[element.elementName] = state.error; - this.#updateCombinedErrorText(errorTextElement.id, errorTextMap); - window.parent.postMessage( - { - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, - data: { height: rootDiv.scrollHeight, name: window.name }, - }, - this.clientMetaData.clientDomain, - ); - }); - } - - rowDiv.append(elementDiv); - }); - rootDiv.append(rowDiv); - if (isComposableContainer) { rootDiv.append(errorTextElement); } - }); - - if (this.#domForm) { - // for cleaning - this.#domForm.innerHTML = ''; - document.body.innerHTML = ''; - this.#domForm.append(rootDiv); - document.body.append(this.#domForm); - } - bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name, (data, callback) => { - callback({ height: rootDiv.scrollHeight, name: window.name }); - }); - window.parent.postMessage( - { - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, - data: { height: rootDiv.scrollHeight, name: window.name }, - }, - this.clientMetaData.clientDomain, - ); - window.addEventListener('message', (event) => { - if (event?.data?.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name) { - window.parent.postMessage( - { - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, - data: { height: rootDiv.scrollHeight, name: window.name }, - }, - this.clientMetaData.clientDomain, - ); - } - }); - }; - - #updateCombinedErrorText = (elementId, errorMessages) => { - const currentErrorElememt = document.getElementById(elementId); - let errorText = ''; - Object.values(errorMessages).forEach((message) => { - errorText += (message) && `${message}. `; - }); - if (currentErrorElememt) { currentErrorElememt.innerText = errorText; } - }; -} diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts deleted file mode 100644 index 6b5d08aa0..000000000 --- a/src/core/internal/internal-types/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ClientToJSON } from '../../../client'; -import EventEmitter from '../../../event-emitter'; -import { CollectContainer, ComposableContainer, RevealContainer } from '../../../index-node'; -import { ContainerType } from '../../../skyflow'; -import { CollectElementOptions, ICollectOptions } from '../../../utils/common'; -import { ElementType } from '../../constants'; -import SkyflowContainer from '../../external/skyflow-container'; - -export interface ElementInfo { - frameId: string; - elementId: string; -} - -export interface TokenizeDataInput extends ICollectOptions{ - type: string; - elementIds: Array; - containerId: string; -} - -export interface UploadFileDataInput extends ICollectOptions { - type: string; - elementIds: Array; - containerId: string; -} - -export interface BatchInsertRequestBody { - method: string; - quorum?: boolean; - tableName: string; - fields?: Record; - upsert?: string; - ID?: string; - tokenization?: boolean; - [key: string]: any; -} - -export interface ContainerProps { - containerId: string; - isMounted: boolean; - type: string; -} - -export interface RevealContainerProps { - containerId: string; - isMounted: boolean; - eventEmitter: EventEmitter; - type: string; -} - -export interface InternalState { - metaData: any; - isEmpty: boolean, - isValid: boolean, - isFocused: boolean, - isRequired: boolean, - name: string; - elementType: ElementType; - isComplete: boolean; - value: string | Blob | undefined; - selectedCardScheme: string; -} - -export interface FormattedCollectElementOptions extends CollectElementOptions { - [key: string]: any; -} - -export interface SkyflowElementProps { - id: string; - type: ElementType; - element: HTMLElement; - container: CollectContainer | RevealContainer | ComposableContainer; -} - -export interface ClientMetadata { - uuid: string, - clientDomain: string, - sdkVersion?: string; - sessionId?: string; -} - -export interface Metadata extends ClientMetadata { - clientJSON: ClientToJSON; - containerType: ContainerType; - skyflowContainer: SkyflowContainer; - getSkyflowBearerToken: () => Promise; -} diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts deleted file mode 100644 index 90b7c8fed..000000000 --- a/src/core/internal/reveal/reveal-frame.ts +++ /dev/null @@ -1,732 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -import bus from 'framebus'; -import { - ELEMENT_EVENTS_TO_IFRAME, - STYLE_TYPE, - REVEAL_ELEMENT_ERROR_TEXT, - REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, - REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES, - REVEAL_ELEMENT_DIV_STYLE, - REVEAL_ELEMENT_OPTIONS_TYPES, - COPY_UTILS, - REVEAL_COPY_ICON_STYLES, - RENDER_ELEMENT_IMAGE_STYLES, - DEFAULT_FILE_RENDER_ERROR, - ELEMENT_EVENTS_TO_CLIENT, - REVEAL_TYPES, - SIGNED_TOKEN_PREFIX, -} from '../../constants'; -import getCssClassesFromJss, { generateCssWithoutClass } from '../../../libs/jss-styles'; -import { - printLog, parameterizedString, -} from '../../../utils/logs-helper'; -import logs from '../../../utils/logs'; -import { - Context, IRenderResponseType, IRevealRecord, MessageType, RedactionType, -} from '../../../utils/common'; -import { - constructMaskTranslation, - formatRevealElementOptions, - getAtobValue, - getContainerType, - getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, -} from '../../../utils/helpers'; -import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../../core-utils/reveal'; -import Client from '../../../client'; -import properties from '../../../properties'; - -const { getType } = require('mime'); - -const CLASS_NAME = 'RevealFrame'; -class RevealFrame { - static revealFrame: RevealFrame; - - #elementContainer: HTMLDivElement; - - #dataElememt: HTMLSpanElement; - - #labelElement: HTMLSpanElement; - - #errorElement: HTMLSpanElement; - - #name: string; - - #record: any; - - #containerId: string; - - #clientDomain: string; - - #inputStyles!: object; - - #labelStyles!: object; - - #errorTextStyles!: object; - - #revealedValue!: string; - - #context: Context; - - private domCopy?: HTMLImageElement; - - private isRevealCalled?: boolean; - - #skyflowContainerId: string = ''; - - #client!: Client; - - #composableContainer: Boolean = false; - - static init() { - const url = window.location?.href; - const configIndex = url.indexOf('?'); - const encodedString = configIndex !== -1 ? decodeURIComponent(url.substring(configIndex + 1)) : ''; - const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; - const skyflowContainerId = parsedRecord.clientJSON.metaData.uuid; - RevealFrame.revealFrame = new RevealFrame(parsedRecord.record, - parsedRecord.context, skyflowContainerId); - } - - constructor(record, context: Context, id: string, rootDiv?: HTMLDivElement) { - this.#skyflowContainerId = id; - this.#name = rootDiv ? record?.name : window.name; - this.#composableContainer = getContainerType(this.#name) === 'COMPOSABLE_REVEAL'; - this.#containerId = getValueFromName(this.#name, 2); - const encodedClientDomain = getValueFromName(this.#name, 4); - const clientDomain = getAtobValue(encodedClientDomain); - this.#clientDomain = document.referrer.split('/').slice(0, 3).join('/') || clientDomain; - this.#record = record; - this.#context = context; - this.isRevealCalled = false; - - this.#elementContainer = document.createElement('div'); - this.#elementContainer.className = 'SkyflowElement-div-container'; - getCssClassesFromJss(REVEAL_ELEMENT_DIV_STYLE, 'div'); - - this.#labelElement = document.createElement('span'); - this.#labelElement.className = `SkyflowElement-${this.#name}-label-${STYLE_TYPE.BASE}`; - - this.#dataElememt = document.createElement('span'); - this.#dataElememt.className = `SkyflowElement-${this.#name}-content-${STYLE_TYPE.BASE}`; - this.#dataElememt.id = this.#name; - - this.#errorElement = document.createElement('span'); - this.#errorElement.className = `SkyflowElement-${this.#name}-error-${STYLE_TYPE.BASE}`; - - if (this.#record.enableCopy) { - this.domCopy = document.createElement('img'); - this.domCopy.src = COPY_UTILS.copyIcon; - this.domCopy.title = COPY_UTILS.toCopy; - this.domCopy.setAttribute('style', this.#record?.inputStyles?.copyIcon ? styleToString(this.#record.inputStyles.copyIcon) : REVEAL_COPY_ICON_STYLES); - this.#elementContainer.append(this.domCopy); - - this.domCopy.onclick = () => { - if (this.isRevealCalled) { - handleCopyIconClick(this.#revealedValue, this.domCopy); - } else { - handleCopyIconClick(this.#record.token, this.domCopy); - } - }; - } - if (Object.prototype.hasOwnProperty.call(this.#record, 'label') && !Object.prototype.hasOwnProperty.call(this.#record, 'skyflowID')) { - this.#labelElement.innerText = this.#record.label; - this.#elementContainer.append(this.#labelElement); - - if (Object.prototype.hasOwnProperty.call(this.#record, 'labelStyles')) { - this.#labelStyles = {}; - this.#labelStyles[STYLE_TYPE.BASE] = { - ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], - ...this.#record.labelStyles[STYLE_TYPE.BASE], - }; - // getCssClassesFromJss(this.#labelStyles, 'label'); - getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); - - if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { - generateCssWithoutClass(this.#record.labelStyles[STYLE_TYPE.GLOBAL]); - } - } else { - getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, `${this.#name}-label`); - } - } - this.updateDataView(); - if (Object.prototype.hasOwnProperty.call(this.#record, 'inputStyles')) { - this.#inputStyles = {}; - this.#inputStyles[STYLE_TYPE.BASE] = { - ...this.#record.inputStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); - if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { - generateCssWithoutClass(this.#record.inputStyles[STYLE_TYPE.GLOBAL]); - } - } - - if ( - Object.prototype.hasOwnProperty.call(this.#record, 'errorTextStyles') - && Object.prototype.hasOwnProperty.call(this.#record.errorTextStyles, STYLE_TYPE.BASE) - ) { - this.#errorTextStyles = {}; - this.#errorTextStyles[STYLE_TYPE.BASE] = { - ...REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES[STYLE_TYPE.BASE], - ...this.#record.errorTextStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); - if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { - generateCssWithoutClass(this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]); - } - } else { - getCssClassesFromJss( - REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES, - `${this.#name}-error`, - ); - } - - this.#elementContainer.appendChild(this.#dataElememt); - - if (rootDiv) rootDiv.append(this.#elementContainer); - else document.body.append(this.#elementContainer); - // document.body.append(this.#elementContainer); - - bus.emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, { name: this.#name }); - if (rootDiv) { - this.getConfig(); - window.parent.postMessage({ - type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, - data: { - name: this.#name, - }, - }, this.#clientDomain); - - window.parent.postMessage({ - type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - data: { height: this.#elementContainer.scrollHeight, name: this.#name }, - }, this.#clientDomain); - } - bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, (_, callback) => { - callback({ height: this.#elementContainer.scrollHeight, name: this.#name }); - }); - const sub2 = (responseUrl) => { - if (responseUrl.iframeName === this.#name) { - if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { - this.setRevealError(DEFAULT_FILE_RENDER_ERROR); - if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { - this.#dataElememt.innerText = this.#record.altText; - } - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, - ); - } else { - const ext = this.getExtension(responseUrl.url); - this.addFileRender(responseUrl.url, ext); - } - } - }; - bus - .target(window.location.origin) - .on( - ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, - sub2, - ); - - const sub = (data) => { - const tokenToMatch = this.decodeSignedToken(this.#record.token); - - if (Object.prototype.hasOwnProperty.call(data, tokenToMatch)) { - let responseData = data[tokenToMatch]; - - if (Array.isArray(responseData)) { - const recordRedaction = this.#record.redaction || RedactionType.PLAIN_TEXT; - const matchingRecord = responseData.find( - (item) => item.redaction === recordRedaction, - ); - responseData = matchingRecord || responseData[0]; - } - - const responseValue = typeof responseData === 'string' ? responseData : responseData?.value; - this.#revealedValue = responseValue; - this.isRevealCalled = true; - this.#dataElememt.innerText = responseValue; - if (this.#record.mask) { - const { formattedOutput } = getMaskedOutput(this.#dataElememt.innerText, - this.#record.mask[0], - constructMaskTranslation(this.#record.mask)); - this.#dataElememt.innerText = formattedOutput; - } - printLog(parameterizedString(logs.infoLogs.ELEMENT_REVEALED, - CLASS_NAME, tokenToMatch), MessageType.LOG, this.#context?.logLevel); - - // bus - // .target(window.location.origin) - // .off( - // ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId, - // sub, - // ); - } else { - // eslint-disable-next-line no-lonely-if - if (!Object.prototype.hasOwnProperty.call(this.#record, 'skyflowID')) { - this.setRevealError(REVEAL_ELEMENT_ERROR_TEXT); - } - } - // this.updateDataView(); - }; - - bus - .target(window.location.origin) - .on( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId, - sub, - ); - - bus - .target(this.#clientDomain) - .on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#name, (data) => { - if (this.#name === data.name) { - if (data.isTriggerError) { this.setRevealError(data.clientErrorText as string); } else { this.setRevealError(''); } - } - }); - window.parent.postMessage( - { - type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#name, - data: { - name: window.name, - }, - }, this.#clientDomain, - ); - this.updateRevealElementOptions(); - window.addEventListener('message', (event) => { - if (event?.origin === this.#clientDomain) { - if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#name) { - if (event?.data?.data?.iframeName === this.#name - && event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { - this.renderFile(this.#record, event?.data?.clientConfig, - event?.data?.errorMessages)?.then((resolvedResult) => { - const result = formatForRenderClient( - resolvedResult as IRenderResponseType, - this.#record?.column, - ); - window?.parent?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, - data: { - type: REVEAL_TYPES.RENDER_FILE, - result, - }, - }, this.#clientDomain); - - window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, - }, properties?.IFRAME_SECURE_ORIGIN); - })?.catch((error) => { - window?.parent?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, - data: { - type: REVEAL_TYPES.RENDER_FILE, - result: { - errors: error, - }, - }, - }, this.#clientDomain); - - window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, - }, properties?.IFRAME_SECURE_ORIGIN); - }); - } - } - } - - if (event?.data?.type === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name) { - if (event?.data?.data?.height) { - window?.parent?.postMessage({ - type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - data: { - height: this.#elementContainer?.scrollHeight ?? 0, - name: this.#name, - }, - }, this.#clientDomain); - } - } - }); - } - - responseUpdate = (data) => { - if (data?.frameId === this.#record?.name && data?.error) { - if (!Object.prototype.hasOwnProperty.call(this.#record, 'skyflowID')) { - this.setRevealError(REVEAL_ELEMENT_ERROR_TEXT); - } - } else if (data?.frameId === this.#record?.name && data?.[0]?.token) { - const tokenToMatch = this.decodeSignedToken(this.#record?.token); - - if (tokenToMatch === data?.[0]?.token) { - const responseValue = data?.[0]?.value as string ?? ''; - this.#revealedValue = responseValue; - this.isRevealCalled = true; - this.#dataElememt.innerText = responseValue; - - if (this.#record?.mask) { - const { formattedOutput } = getMaskedOutput( - this.#dataElememt?.innerText ?? '', - this.#record?.mask?.[0], - constructMaskTranslation(this.#record?.mask), - ); - this.#dataElememt.innerText = formattedOutput ?? ''; - } - - printLog( - parameterizedString( - logs?.infoLogs?.ELEMENT_REVEALED, - CLASS_NAME, - tokenToMatch, - ), - MessageType.LOG, - this.#context?.logLevel, - ); - } - } - - window?.parent?.postMessage( - { - type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - data: { - height: this.#elementContainer?.scrollHeight ?? 0, - name: this.#name, - }, - }, - this.#clientDomain, - ); - }; - - getConfig = () => { - const url = window.location?.href; - const configIndex = url.indexOf('?'); - const encodedString = configIndex !== -1 ? decodeURIComponent(url.substring(configIndex + 1)) : ''; - const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; - this.#clientDomain = parsedRecord.clientDomain || ''; - this.#containerId = parsedRecord.containerId; - }; - - getData = () => this.#record; - - public decodeSignedToken(token: string): string { - let tokenToMatch = token; - - if (tokenToMatch && tokenToMatch.startsWith(SIGNED_TOKEN_PREFIX)) { - try { - const bearerToken = tokenToMatch.substring(SIGNED_TOKEN_PREFIX.length); - - const parts = bearerToken.split('.'); - if (parts.length === 3) { - const payload = JSON.parse(atob(parts[1])); - - if (payload.tok) { - tokenToMatch = payload.tok; - } - } - } catch (err) { - printLog( - parameterizedString(logs.errorLogs.SIGNED_TOKEN_DECODE_FAILED), - MessageType.ERROR, - this.#context?.logLevel, - ); - } - } - - return tokenToMatch; - } - - private sub2 = (responseUrl: { iframeName?: string; error?: string; url?: string }) => { - if (responseUrl.iframeName === this.#name) { - if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { - this.setRevealError(DEFAULT_FILE_RENDER_ERROR); - if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { - this.#dataElememt.innerText = this.#record.altText; - } - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, - ); - } else { - const ext = this.getExtension(responseUrl.url as string); - this.addFileRender(responseUrl.url as string, ext); - } - } - }; - - private renderFile(data: IRevealRecord, clientConfig, customErrorMessages): - Promise | undefined { - this.#client = new Client(clientConfig, { - uuid: '', - clientDomain: '', - }); - this.#client.setErrorMessages(customErrorMessages ?? {}); - return new Promise((resolve, reject) => { - try { - getFileURLFromVaultBySkyflowIDComposable(data, this.#client, clientConfig.authToken) - .then((resolvedResult) => { - let url = ''; - if (resolvedResult.fields && data.column) { - url = resolvedResult.fields[data.column]; - } - this.sub2({ - url, - iframeName: this.#name, - }); - resolve(resolvedResult); - }, - (rejectedResult) => { - this.sub2({ - error: DEFAULT_FILE_RENDER_ERROR, - iframeName: this.#name, - }); - reject(rejectedResult); - }); - } catch (err) { - reject(err); - } - }); - } - - // eslint-disable-next-line class-methods-use-this - private getExtension(url: string) { - try { - const params = new URL(url).searchParams; - const name = params.get('response-content-disposition'); - if (name) { - const ext = getType(name); - return ext; - } - return ''; - } catch { - return ''; - } - } - - private addFileRender(responseUrl: string, ext: string) { - let tag = ''; - if (typeof ext === 'string' && ext.includes('image')) { - tag = 'img'; - } else { - tag = 'embed'; - } - const fileElement = document.createElement(tag); - fileElement.addEventListener('load', () => { - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, - ); - }); - fileElement.className = `SkyflowElement-${tag}-${STYLE_TYPE.BASE}`; - if (tag === 'embed' && typeof ext === 'string') { - fileElement.setAttribute('type', ext); - } - fileElement.setAttribute('src', responseUrl); - if (Object.prototype.hasOwnProperty.call(this.#record, 'inputStyles')) { - this.#inputStyles = {}; - if (tag === 'img') { - this.#inputStyles[STYLE_TYPE.BASE] = { - ...this.#record.inputStyles[STYLE_TYPE.BASE], - }; - if (this.#record?.inputStyles - && this.#record?.inputStyles[STYLE_TYPE.BASE] - && this.#record?.inputStyles[STYLE_TYPE.BASE]?.overflow && this.#composableContainer) { - this.#elementContainer.className = `SkyflowElement-div-container-${STYLE_TYPE.BASE}`; - const divStyles = { - [STYLE_TYPE.BASE]: { - ...this.#record.inputStyles[STYLE_TYPE.BASE], - }, - }; - this.#elementContainer.style.overflow = this.#record - .inputStyles[STYLE_TYPE.BASE].overflow as string; - this.#inputStyles[STYLE_TYPE.BASE] = { - ...this.#inputStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(divStyles, 'div-container'); - } else { - this.#inputStyles[STYLE_TYPE.BASE] = { - ...RENDER_ELEMENT_IMAGE_STYLES[STYLE_TYPE.BASE], - ...this.#inputStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#inputStyles, tag); - } - } else { - this.#inputStyles[STYLE_TYPE.BASE] = { - ...RENDER_ELEMENT_IMAGE_STYLES[STYLE_TYPE.BASE], - ...this.#record.inputStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#inputStyles, tag); - } - } - - if (this.#elementContainer.childNodes[0] !== undefined) { - this.#elementContainer.innerHTML = ''; - this.#elementContainer.appendChild(fileElement); - } else { - this.#elementContainer.appendChild(fileElement); - } - if (fileElement instanceof HTMLImageElement - && this.#record?.inputStyles - && this.#record?.inputStyles[STYLE_TYPE.BASE] - && this.#record?.inputStyles[STYLE_TYPE.BASE]?.overflow && this.#composableContainer) { - fileElement.onload = () => { - if (fileElement?.naturalWidth && fileElement?.naturalHeight) { - fileElement.style.width = `${fileElement.naturalWidth}px`; - fileElement.style.height = `${fileElement.naturalHeight}px`; - } - - if (this.#record?.inputStyles[STYLE_TYPE.BASE]?.width) { - this.#elementContainer.style.width = this.#record.inputStyles[STYLE_TYPE.BASE].width; - } - if (this.#record?.inputStyles[STYLE_TYPE.BASE]?.height) { - this.#elementContainer.style.height = this.#record.inputStyles[STYLE_TYPE.BASE].height; - } - this.#elementContainer.style.overflow = this.#record - .inputStyles[STYLE_TYPE.BASE].overflow as string; - - window?.postMessage({ - type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, - }, properties?.IFRAME_SECURE_ORIGIN); - }; - } - } - - private setRevealError(errorText: string) { - this.#errorElement.innerText = errorText; - this.#elementContainer.appendChild(this.#errorElement); - } - - private updateRevealElementOptions() { - window.addEventListener('message', (event) => { - if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS - + this.#name) { - const data = event?.data; - if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { - const updatedValue = data.updatedValue as object; - this.#record = { - ...this.#record, - ...updatedValue, - ...formatRevealElementOptions(updatedValue), - }; - this.updateElementProps(); - if (this.isRevealCalled) { - if (this.#record?.mask) { - const { formattedOutput } = getMaskedOutput( - this.#revealedValue ?? '', - this.#record?.mask?.[0], - constructMaskTranslation(this.#record?.mask), - ); - this.#dataElememt.innerText = formattedOutput ?? ''; - } - } - } - } - }); - bus - .target(this.#clientDomain) - .on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#name, (data) => { - if (data.name === this.#name) { - if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT) { - if (data.updatedValue) { - this.#record = { - ...this.#record, - altText: data.updatedValue, - }; - } else { - delete this.#record.altText; - } - - this.updateDataView(); - } else if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN) { - this.#record = { - ...this.#record, - token: data.updatedValue, - }; - this.updateDataView(); - } else if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { - const updatedValue = data.updatedValue as object; - this.#record = { - ...this.#record, - ...updatedValue, - }; - this.updateElementProps(); - } - } - }); - } - - private updateElementProps() { - this.updateDataView(); - if (Object.prototype.hasOwnProperty.call(this.#record, 'label')) { - this.#labelElement.innerText = this.#record.label; - } - if (Object.prototype.hasOwnProperty.call(this.#record, 'inputStyles')) { - this.#inputStyles[STYLE_TYPE.BASE] = { - ...this.#inputStyles, - ...this.#record.inputStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); - if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { - const newInputGlobalStyles = { - ...this.#inputStyles[STYLE_TYPE.GLOBAL], - ...this.#record.inputStyles[STYLE_TYPE.GLOBAL], - }; - generateCssWithoutClass(newInputGlobalStyles); - } - } - if (Object.prototype.hasOwnProperty.call(this.#record, 'labelStyles')) { - this.#labelStyles[STYLE_TYPE.BASE] = { - ...this.#labelStyles, - ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], - ...this.#record.labelStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); - - if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { - const newLabelGlobalStyles = { - ...this.#labelStyles[STYLE_TYPE.GLOBAL], - ...this.#record.labelStyles[STYLE_TYPE.GLOBAL], - }; - generateCssWithoutClass(newLabelGlobalStyles); - } - } - if (Object.prototype.hasOwnProperty.call(this.#record, 'errorTextStyles')) { - this.#errorTextStyles[STYLE_TYPE.BASE] = { - ...REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES[STYLE_TYPE.BASE], - ...this.#errorTextStyles[STYLE_TYPE.BASE], - ...this.#record.errorTextStyles[STYLE_TYPE.BASE], - }; - getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); - if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { - const newErrorTextGlobalStyles = { - ...this.#errorTextStyles[STYLE_TYPE.GLOBAL], - ...this.#record.errorTextStyles[STYLE_TYPE.GLOBAL], - }; - generateCssWithoutClass(newErrorTextGlobalStyles); - } - } - } - - private updateDataView() { - if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { - this.#dataElememt.innerText = this.#record.altText; - } else if (this.#revealedValue) { - this.#dataElememt.innerText = this.#revealedValue; - } else if (Object.prototype.hasOwnProperty.call(this.#record, 'token')) { - this.#dataElememt.innerText = this.#record.token; - } - } -} - -export default RevealFrame; diff --git a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts deleted file mode 100644 index c96a9b780..000000000 --- a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts +++ /dev/null @@ -1,960 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -import bus from 'framebus'; -import get from 'lodash/get'; -import Client from '../../../client'; -import { - checkForElementMatchRule, - checkForValueMatch, - constructElementsInsertReq, - constructInsertRecordRequest, - constructInsertRecordResponse, - constructUpdateRecordRequest, - constructUpdateRecordResponse, - constructUploadResponse, - updateRecordsBySkyflowID, -} from '../../../core-utils/collect'; -import { - fetchRecordsGET, - fetchRecordsByTokenId, - fetchRecordsBySkyflowID, - getFileURLFromVaultBySkyflowID, - formatRecordsForClient, - formatRecordsForIframe, -} from '../../../core-utils/reveal'; -import { getAccessToken } from '../../../utils/bus-events'; -import { - COLLECT_TYPES, - CORALOGIX_DOMAIN, - DEFAULT_FILE_RENDER_ERROR, DOMAIN, - ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, PUREJS_TYPES, REVEAL_TYPES, SDK_IFRAME_EVENT, -} from '../../constants'; -import { printLog, parameterizedString } from '../../../utils/logs-helper'; -import logs from '../../../utils/logs'; -import { - IRevealRecord, - IGetRecord, - MessageType, - Context, - ISkyflowIdRecord, - IGetOptions, - IInsertRecordInput, - IInsertOptions, - UploadFilesResponse, - RevealResponse, - InsertResponse, - CollectResponse, - IRevealResponseType, - GetResponse, - GetByIdResponse, - IDeleteResponseType, - IDeleteRecordInput, - IRenderResponseType, - IUpdateRequest, - UpdateResponse, - IUpdateOptions, - ErrorType, -} from '../../../utils/common'; -import { deleteData } from '../../../core-utils/delete'; -import properties from '../../../properties'; -import { - fileValidation, generateUploadFileName, - getAtobValue, getSDKNameAndVersion, getValueFromName, vaildateFileName, -} from '../../../utils/helpers'; -import SkyflowError from '../../../libs/skyflow-error'; -import SKYFLOW_ERROR_CODE from '../../../utils/constants'; -import { - BatchInsertRequestBody, ElementInfo, TokenizeDataInput, UploadFileDataInput, -} from '../internal-types'; -import IFrameFormElement from '../iframe-form'; - -const set = require('set-value'); - -const CLASS_NAME = 'SkyflowFrameController'; -class SkyflowFrameController { - #clientId: string; - - #clientDomain: string; - - #client!: Client; - - #context!: Context; - - constructor(clientId: string) { - this.#clientId = clientId || ''; - const encodedClientDomain = getValueFromName(window.name, 2); - const clientDomain = getAtobValue(encodedClientDomain); - this.#clientDomain = document.referrer.split('/').slice(0, 3).join('/') || clientDomain; - bus - .on( - ELEMENT_EVENTS_TO_IFRAME.PUSH_EVENT + this.#clientId, - (data: any) => { - if (window?.CoralogixRum - && !window.CoralogixRum.isInited - && this.#client?.config?.options?.trackingKey - && this.#client?.config?.options?.trackingKey.length >= 35) { - const sdkMetaData = getSDKNameAndVersion(this.#client?.toJSON()?.metaData?.sdkVersion); - window.CoralogixRum.init({ - application: sdkMetaData.sdkName, - public_key: this.#client.config?.options?.trackingKey, - coralogixDomain: DOMAIN, - version: sdkMetaData.sdkVersion, - beforeSend: (event: any) => { - if (event?.log_context?.message && event.log_context.message === SDK_IFRAME_EVENT) { - return event; - } - return null; - }, - }); - } - if (data && data.event && window?.CoralogixRum) { - try { - window.CoralogixRum.info(SDK_IFRAME_EVENT, data.event); - printLog(parameterizedString(logs.infoLogs.METRIC_CAPTURE_EVENT), - MessageType.LOG, this.#context?.logLevel); - } catch (err: any) { - printLog(parameterizedString(logs.infoLogs.UNKNOWN_METRIC_CAPTURE_EVENT, - err.toString()), - MessageType.LOG, this.#context?.logLevel); - } - } - }, - ); - bus - .target(this.#clientDomain) - .on( - ELEMENT_EVENTS_TO_IFRAME.PUREJS_REQUEST + this.#clientId, - (data, callback) => { - printLog( - parameterizedString( - logs.infoLogs.CAPTURE_PURE_JS_REQUEST, - CLASS_NAME, - data.type, - ), - MessageType.LOG, - this.#context.logLevel, - ); - - if (data.type === PUREJS_TYPES.DETOKENIZE) { - fetchRecordsByTokenId( - data.records as IRevealRecord[], - this.#client, - true, - ).then( - (resolvedResult: IRevealResponseType) => { - printLog( - parameterizedString( - logs.infoLogs.FETCH_RECORDS_RESOLVED, - CLASS_NAME, - ), - MessageType.LOG, - this.#context.logLevel, - ); - callback(resolvedResult); - }, - (rejectedResult: IRevealResponseType) => { - printLog( - parameterizedString(logs.errorLogs.FETCH_RECORDS_REJECTED), - MessageType.ERROR, - this.#context.logLevel, - ); - - callback({ error: rejectedResult }); - }, - ); - } else if (data.type === PUREJS_TYPES.INSERT) { - this.insertData(data.records as IInsertRecordInput, data.options as IInsertOptions) - .then((result: InsertResponse) => { - printLog( - parameterizedString( - logs.infoLogs.INSERT_RECORDS_RESOLVED, - CLASS_NAME, - ), - MessageType.LOG, - this.#context.logLevel, - ); - callback(result); - }) - .catch((error: InsertResponse) => { - printLog( - parameterizedString(logs.errorLogs.INSERT_RECORDS_REJECTED), - MessageType.ERROR, - this.#context.logLevel, - ); - callback({ error }); - }); - } else if (data.type === PUREJS_TYPES.UPDATE) { - this.updateData(data.record as IUpdateRequest, data.options as IUpdateOptions) - .then((result: any) => { - printLog( - parameterizedString( - logs.infoLogs.UPDATE_RECORD_RESOLVED, - CLASS_NAME, - ), - MessageType.LOG, - this.#context.logLevel, - ); - callback(result); - }) - .catch((error: any) => { - printLog( - parameterizedString(logs.errorLogs.UPDATE_RECORD_REJECTED), - MessageType.ERROR, - this.#context.logLevel, - ); - callback({ error }); - }); - } else if (data.type === PUREJS_TYPES.GET) { - fetchRecordsGET( - data.records as IGetRecord[], this.#client, data.options as IGetOptions, - ).then( - (resolvedResult: GetResponse) => { - printLog( - parameterizedString(logs.infoLogs.GET_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel, - ); - - callback(resolvedResult); - }, - (rejectedResult: GetResponse) => { - printLog(parameterizedString( - logs.errorLogs.GET_REJECTED, - ), - MessageType.ERROR, - this.#context.logLevel); - - callback({ error: rejectedResult }); - }, - ); - } else if (data.type === PUREJS_TYPES.GET_BY_SKYFLOWID) { - fetchRecordsBySkyflowID( - data.records as ISkyflowIdRecord[], - this.#client, - ).then( - (resolvedResult: GetByIdResponse) => { - printLog( - parameterizedString( - logs.infoLogs.GET_BY_SKYFLOWID_RESOLVED, - CLASS_NAME, - ), - MessageType.LOG, - this.#context.logLevel, - ); - - callback(resolvedResult); - }, - (rejectedResult: GetByIdResponse) => { - printLog( - parameterizedString(logs.errorLogs.GET_BY_SKYFLOWID_REJECTED), - MessageType.ERROR, - this.#context.logLevel, - ); - - callback({ error: rejectedResult }); - }, - ); - } else if (data.type === PUREJS_TYPES.DELETE) { - deleteData( - data.records as IDeleteRecordInput, - data.options || {}, - this.#client, - ).then( - (resolvedResult: IDeleteResponseType) => { - printLog( - parameterizedString( - logs.infoLogs.DELETE_RESOLVED, - CLASS_NAME, - ), - MessageType.LOG, - this.#context.logLevel, - ); - - callback(resolvedResult); - }, - ).catch((rejectedResult: IDeleteResponseType) => { - printLog( - parameterizedString( - logs.errorLogs.DELETE_RECORDS_REJECTED, - ), - MessageType.ERROR, - this.#context.logLevel, - ); - - callback({ error: rejectedResult }); - }); - } - }, - ); - bus - .target(this.#clientDomain) - .emit(ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + this.#clientId, {}, (data: any) => { - this.#context = data.context; - data.client.config = { - ...data.client.config, - }; - this.#client = Client.fromJSON(data.client) as any; - Object.keys(PUREJS_TYPES).forEach((key) => { - printLog(parameterizedString(logs.infoLogs.LISTEN_PURE_JS_REQUEST, - CLASS_NAME, PUREJS_TYPES[key]), MessageType.LOG, this.#context.logLevel); - }); - }); - bus - .target(this.#clientDomain) - .on(ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#clientId, (data, callback) => { - if (this.#client && data?.errorMessages) { - const errorMessages: Partial> = data?.errorMessages; - this.#client.setErrorMessages(errorMessages as Record); - } - printLog( - parameterizedString( - logs.infoLogs.CAPTURE_PURE_JS_REQUEST, - CLASS_NAME, - data.type, - ), - MessageType.LOG, - this.#context.logLevel, - ); - if (data.type === COLLECT_TYPES.COLLECT) { - printLog( - parameterizedString(logs.infoLogs.CAPTURE_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, - this.#context.logLevel, - ); - const tokenizeDataInput: TokenizeDataInput = { - ...data, - type: data.type, - elementIds: data.elementIds as Array, - containerId: data.containerId as string, - }; - this.tokenize(tokenizeDataInput) - .then((response: CollectResponse) => { - callback(response); - }) - .catch((error: CollectResponse) => { - callback({ error }); - }); - } else if (data.type === COLLECT_TYPES.FILE_UPLOAD) { - printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), - MessageType.LOG, this.#context.logLevel); - const uploadFilesDataInput = { - ...data, - type: data.type, - elementIds: data.elementIds as string[], - containerId: data.containerId as string, - }; - this.parallelUploadFiles(uploadFilesDataInput) - .then((response: UploadFilesResponse) => { - callback(response); - }) - .catch((error: UploadFilesResponse) => { - callback({ error }); - }); - } - }); - bus - .target(this.#clientDomain) - .emit(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#clientId, - {}, (data: any) => { - this.#context = data.context; - data.client.config = { - ...data.client.config, - }; - this.#client = Client.fromJSON(data.client) as any; - Object.keys(COLLECT_TYPES).forEach((key) => { - printLog(parameterizedString(logs.infoLogs.LISTEN_PURE_JS_REQUEST, - CLASS_NAME, COLLECT_TYPES[key]), MessageType.LOG, this.#context.logLevel); - }); - Object.keys(REVEAL_TYPES).forEach((key) => { - printLog(parameterizedString(logs.infoLogs.LISTEN_PURE_JS_REQUEST, - CLASS_NAME, REVEAL_TYPES[key]), MessageType.LOG, this.#context.logLevel); - }); - }); - bus - .target(this.#clientDomain) - .on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#clientId, (data, callback) => { - printLog( - parameterizedString( - logs.infoLogs.CAPTURE_PURE_JS_REQUEST, - CLASS_NAME, - data.type, - ), - MessageType.LOG, - this.#context.logLevel, - ); - if (this.#client && data?.errorMessages) { - const errorMessages: Partial> = data?.errorMessages; - this.#client.setErrorMessages(errorMessages as Record); - } - - if (data.type === REVEAL_TYPES.REVEAL) { - printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.REVEAL_REQUEST), - MessageType.LOG, this.#context.logLevel); - this.revealData(data.records as IRevealRecord[], data.containerId as string).then( - (resolvedResult) => { - callback(resolvedResult); - }, - (rejectedResult) => { - callback({ error: rejectedResult }); - }, - ); - } else if (data.type === REVEAL_TYPES.RENDER_FILE) { - printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), - MessageType.LOG, this.#context.logLevel); - this.renderFile(data.records as IRevealRecord, data.iframeName as string).then( - (resolvedResult) => { - callback( - resolvedResult, - ); - }, - (rejectedResult) => { - callback({ errors: rejectedResult }); - }, - ); - } - }); - } - - static init(clientId: string): SkyflowFrameController { - const trackingStatus = getValueFromName(window.name, 3) === 'true'; - if (trackingStatus) { - const scriptTag = document.createElement('script'); - scriptTag.src = CORALOGIX_DOMAIN; - document.head.append(scriptTag); - } - return new SkyflowFrameController(clientId); - } - - revealData(revealRecords: IRevealRecord[], containerId: string): Promise { - const id = containerId; - return new Promise((resolve, reject) => { - fetchRecordsByTokenId(revealRecords, this.#client, false).then( - (resolvedResult) => { - const formattedResult = formatRecordsForIframe(resolvedResult); - bus - .target(properties.IFRAME_SECURE_SITE) - .emit( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY - + id, - formattedResult, - ); - resolve(formatRecordsForClient(resolvedResult)); - }, - (rejectedResult) => { - const formattedResult = formatRecordsForIframe(rejectedResult); - bus - .target(properties.IFRAME_SECURE_SITE) - .emit( - ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY - + id, - formattedResult, - ); - reject(formatRecordsForClient(rejectedResult)); - }, - ); - }); - } - - insertData(records: IInsertRecordInput, options: IInsertOptions): Promise { - const requestBody: Array = constructInsertRecordRequest( - records, options, - ); - return new Promise((rootResolve, rootReject) => { - getAccessToken(this.#clientId).then((authToken) => { - this.#client - .request({ - body: JSON.stringify({ records: requestBody }), - requestMethod: 'POST', - url: - `${this.#client.config.vaultURL}/v1/vaults/${ - this.#client.config.vaultID}`, - headers: { - Authorization: `Bearer ${authToken}`, - }, - - }) - .then((response: any) => { - rootResolve( - constructInsertRecordResponse( - response, - options?.tokens ?? true, - records?.records, - ), - ); - }) - .catch((error) => { - if (error?.error?.type) { - error = { - error: { - code: error?.error?.code, - description: error?.error?.description, - }, - }; - } - rootReject(error); - }); - }).catch((err) => { - rootReject(err); - }); - }); - } - - updateData(updateData: IUpdateRequest, options?: IUpdateOptions): Promise { - const requestBody = constructUpdateRecordRequest( - updateData, options, - ); - return new Promise((rootResolve, rootReject) => { - getAccessToken(this.#clientId).then((authToken) => { - const { table, skyflowID } = updateData; - this.#client - .request({ - body: JSON.stringify(requestBody), - requestMethod: 'PUT', - url: `${this.#client.config.vaultURL}/v1/vaults/${this.#client.config.vaultID}/${table}/${skyflowID}`, - headers: { - Authorization: `Bearer ${authToken}`, - 'content-type': 'application/json', - }, - }) - .then((response: any) => { - rootResolve( - constructUpdateRecordResponse(response, options?.tokens ?? false), - ); - }) - .catch((error: any) => { - rootReject(error); - }); - }).catch((err) => { - rootReject(err); - }); - }); - } - - renderFile(data: IRevealRecord, iframeName: string): Promise { - return new Promise((resolve, reject) => { - try { - getFileURLFromVaultBySkyflowID(data, this.#client) - .then((resolvedResult) => { - let url = ''; - if (resolvedResult.fields && data.column) { - url = resolvedResult.fields[data.column]; - } - bus - .target(properties.IFRAME_SECURE_SITE) - .emit( - ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY - + iframeName, - { - url, - iframeName, - }, - ); - - resolve(resolvedResult); - }, - (rejectedResult) => { - bus - .target(properties.IFRAME_SECURE_SITE) - .emit( - ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY - + iframeName, - { - error: DEFAULT_FILE_RENDER_ERROR, - iframeName, - }, - ); - reject(rejectedResult); - }); - } catch (err) { - reject(err); - } - }); - } - - tokenize = (options: TokenizeDataInput): Promise => { - const id: string = options.containerId; - if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const insertResponseObject: any = {}; - const updateResponseObject: any = {}; - let errorMessage = ''; - for (let i = 0; i < options.elementIds.length; i += 1) { - const Frame = window.parent.frames[`${options.elementIds[i].frameId}:${id}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`]; - const inputElement = Frame.document - .getElementById(options.elementIds[i].elementId); - if (inputElement) { - if ( - inputElement.iFrameFormElement.fieldType - !== ELEMENTS.FILE_INPUT.name && inputElement.iFrameFormElement.fieldType - !== ELEMENTS.MULTI_FILE_INPUT.name - ) { - const { - state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, - setValue, - } = inputElement.iFrameFormElement; - if (state.isRequired || !state.isValid) { - onFocusChange(false); - } - if (validations - && checkForElementMatchRule(validations) - && checkForValueMatch(validations, inputElement.iFrameFormElement)) { - setValue(state.value); - onFocusChange(false); - } - if (!state.isValid || !state.isComplete) { - if (doesClientHasError) { - errorMessage += `${state.name}:${clientErrorText}`; - } else { errorMessage += `${state.name}:${errorText} `; } - } - } - } - } - - if (errorMessage.length > 0) { - return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.COMPLETE_AND_VALID_INPUTS, [`${errorMessage}`], true)); - } - - for (let i = 0; i < options.elementIds.length; i += 1) { - const Frame = window.parent.frames[`${options.elementIds[i].frameId}:${id}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`]; - const inputElement = Frame.document - .getElementById(options.elementIds[i].elementId); - if (inputElement) { - const { - state, tableName, validations, skyflowID, - } = inputElement.iFrameFormElement; - if (tableName) { - if ( - inputElement.iFrameFormElement.fieldType - !== ELEMENTS.FILE_INPUT.name - && inputElement.iFrameFormElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name - ) { - if ( - inputElement.iFrameFormElement.fieldType - === ELEMENTS.checkbox.name - ) { - if (insertResponseObject[state.name]) { - insertResponseObject[state.name] = `${insertResponseObject[state.name]},${state.value - }`; - } else { - insertResponseObject[state.name] = state.value; - } - } else if (insertResponseObject[tableName] && !(skyflowID === '') && skyflowID === undefined) { - if (get(insertResponseObject[tableName], state.name) - && !(validations && checkForElementMatchRule(validations))) { - return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, - [state.name, tableName], true)); - } - set( - insertResponseObject[tableName], - state.name, - inputElement.iFrameFormElement.getUnformattedValue(), - ); - } else if (skyflowID || skyflowID === '') { - if (skyflowID === '' || skyflowID === null) { - return Promise.reject(new SkyflowError( - SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_IN_ADDITIONAL_FIELDS, - )); - } - if (updateResponseObject[skyflowID]) { - set( - updateResponseObject[skyflowID], - state.name, - inputElement.iFrameFormElement.getUnformattedValue(), - ); - } else { - updateResponseObject[skyflowID] = {}; - set( - updateResponseObject[skyflowID], - state.name, - inputElement.iFrameFormElement.getUnformattedValue(), - ); - set( - updateResponseObject[skyflowID], - 'table', - tableName, - ); - } - } else { - insertResponseObject[tableName] = {}; - set( - insertResponseObject[tableName], - state.name, - inputElement.iFrameFormElement.getUnformattedValue(), - ); - } - } - } - } - } - let finalInsertRequest: Array; - let finalInsertRecords; - let finalUpdateRecords; - let insertResponse: InsertResponse; - let updateResponse: InsertResponse; - let insertErrorResponse: any; - let updateErrorResponse; - let insertDone = false; - let updateDone = false; - try { - [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( - insertResponseObject, updateResponseObject, options, - ); - finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options); - } catch (error:any) { - return Promise.reject({ - error: error?.message, - }); - } - const client = this.#client; - const sendRequest = (): Promise => new Promise((rootResolve, rootReject) => { - const clientId = client.toJSON()?.metaData?.uuid || ''; - getAccessToken(clientId).then((authToken) => { - if (finalInsertRequest.length !== 0) { - client - .request({ - body: JSON.stringify({ records: finalInsertRequest }), - requestMethod: 'POST', - url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, - headers: { - authorization: `Bearer ${authToken}`, - 'content-type': 'application/json', - }, - }) - .then((response: any) => { - insertResponse = constructInsertRecordResponse( - response, - options.tokens ?? true, - finalInsertRecords.records, - ); - insertDone = true; - if (finalUpdateRecords.updateRecords.length === 0) { - rootResolve(insertResponse); - } - if (updateDone && updateErrorResponse !== undefined) { - if (updateErrorResponse.records === undefined) { - updateErrorResponse.records = insertResponse.records; - } else { - updateErrorResponse.records = (insertResponse.records || []) - .concat(updateErrorResponse.records); - } - rootReject(updateErrorResponse); - } else if (updateDone && updateResponse !== undefined) { - rootResolve( - { records: (insertResponse.records || []).concat(updateResponse.records || []) }, - ); - } - }) - .catch((error) => { - insertDone = true; - if (finalUpdateRecords.updateRecords.length === 0) { - rootReject(error); - } else { - insertErrorResponse = { - errors: [ - { - error: { - code: error?.error?.code, - description: error?.error?.description, - type: error?.error?.type, - }, - }, - ], - }; - } - if (updateDone && updateResponse !== undefined) { - const errors = insertErrorResponse.errors; - const records = updateResponse.records; - rootReject({ errors, records }); - } else if (updateDone && updateErrorResponse !== undefined) { - updateErrorResponse.errors = updateErrorResponse.errors - .concat(insertErrorResponse.errors); - rootReject(updateErrorResponse); - } - }); - } - if (finalUpdateRecords.updateRecords.length !== 0) { - updateRecordsBySkyflowID(finalUpdateRecords, client, options) - .then((response: any) => { - updateResponse = { - records: response, - }; - updateDone = true; - if (finalInsertRequest.length === 0) { - rootResolve(updateResponse); - } - if (insertDone && insertResponse !== undefined) { - rootResolve( - { records: (insertResponse.records || []).concat(updateResponse.records || []) }, - ); - } else if (insertDone && insertErrorResponse !== undefined) { - const errors = insertErrorResponse.errors; - const records = updateResponse.records; - rootReject({ errors, records }); - } - }).catch((error) => { - updateErrorResponse = error; - updateDone = true; - if (finalInsertRequest.length === 0) { - rootReject(error); - } - if (insertDone && insertResponse !== undefined) { - if (updateErrorResponse.records === undefined) { - updateErrorResponse.records = insertResponse.records; - } else { - updateErrorResponse.records = (insertResponse.records || []) - .concat(updateErrorResponse.records); - } - rootReject(updateErrorResponse); - } else if (insertDone && insertErrorResponse !== undefined) { - updateErrorResponse.errors = updateErrorResponse.errors - .concat(insertErrorResponse.errors); - rootReject(updateErrorResponse); - } - }); - } - }).catch((err) => { - rootReject(err); - }); - }); - - return new Promise((resolve, reject) => { - sendRequest() - .then((res) => resolve(res)) - .catch((err) => reject(err)); - }); - }; - - parallelUploadFiles = (options: UploadFileDataInput): - Promise => new Promise((rootResolve, rootReject) => { - const id = options.containerId; - const promises: Promise[] = []; - for (let i = 0; i < options.elementIds.length; i += 1) { - let res: Promise; - const Frame = window.parent.frames[`${options.elementIds[i]}:${id}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`]; - const inputElement = Frame.document - .getElementById(options.elementIds[i]); - if (inputElement) { - if ( - inputElement.iFrameFormElement.fieldType - === ELEMENTS.FILE_INPUT.name - ) { - res = this.uploadFiles(inputElement.iFrameFormElement); - promises.push(res); - } - } - } - Promise.allSettled( - promises, - ).then((resultSet) => { - const fileUploadResponse: Record[] = []; - const errorResponse: Record[] = []; - resultSet.forEach((result) => { - if (result.status === 'fulfilled') { - if (result.value !== undefined && result.value !== null) { - if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { - errorResponse.push(result.value); - } else { - fileUploadResponse.push(result.value); - } - } - } else if (result.status === 'rejected') { - errorResponse.push(result.reason); - } - }); - if (errorResponse.length === 0) { - rootResolve({ fileUploadResponse }); - } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); - else rootReject({ fileUploadResponse, errorResponse }); - }); - }); - - uploadFiles = (fileElement: IFrameFormElement) => { - if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const fileUploadObject: any = {}; - - const { - state, tableName, skyflowID, onFocusChange, preserveFileName, - } = fileElement; - - if (state.isRequired) { - onFocusChange(false); - } - try { - fileValidation(state.value, state.isRequired, fileElement); - } catch (err) { - return Promise.reject(err); - } - - const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); - - if (!validatedFileState) { - return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); - } - fileUploadObject[state.name] = state.value; - - const formData = new FormData(); - - const column = Object.keys(fileUploadObject)[0]; - - const value: Blob = Object.values(fileUploadObject)[0] as Blob; - - formData.append('columnName', column); - formData.append('tableName', tableName ?? ''); - - if (preserveFileName) { - const isValidFileName = vaildateFileName(state.value.name); - if (!isValidFileName) { - return Promise.reject( - new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), - ); - } - formData.append('file', value); - } else { - const generatedFileName = generateUploadFileName(state.value.name); - formData.append('file', new File([value], generatedFileName, { type: state.value.type })); - } - - if (skyflowID) { - formData.append('skyflowID', skyflowID); - } - - const client = this.#client; - const sendRequest = (): - Promise => new Promise((rootResolve, rootReject) => { - const clientId = client.toJSON()?.metaData?.uuid || ''; - getAccessToken(clientId).then((authToken) => { - client - .request({ - body: formData, - requestMethod: 'POST', - url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, - headers: { - authorization: `Bearer ${authToken}`, - 'content-type': 'multipart/form-data', - }, - }) - .then((response: any) => { - rootResolve(constructUploadResponse(response)); - }) - .catch((error) => { - rootReject(error); - }); - }).catch((err) => { - rootReject(err); - }); - }); - - return new Promise((resolve, reject) => { - sendRequest() - .then((res) => resolve(res)) - .catch((err) => { - reject(err); - }); - }); - }; -} -export default SkyflowFrameController; diff --git a/src/index-node.ts b/src/index-node.ts deleted file mode 100644 index 3c2227bc7..000000000 --- a/src/index-node.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright (c) 2025 Skyflow, Inc. -*/ -import Skyflow from './skyflow'; - -export { - IInsertRecordInput as InsertRequest, - IInsertRecord as InsertRecord, - IInsertOptions as InsertOptions, - IUpdateRequest as UpdateRequest, - IUpdateOptions as UpdateOptions, - UpdateResponse, - InsertResponse, - IDetokenizeInput as DetokenizeRequest, - DetokenizeRecord, - DetokenizeResponse, - IDeleteRecordInput as DeleteRequest, - IDeleteRecord as DeleteRecord, - IDeleteOptions as DeleteOptions, - DeleteResponse, - IGetInput as GetRequest, - IGetRecord as GetRecord, - IGetOptions as GetOptions, - GetResponse, - IGetByIdInput as GetByIdRequest, - GetByIdResponse, - ContainerOptions, - CollectElementInput, - CollectElementUpdateOptions, - CollectElementOptions, - ICollectOptions as CollectOptions, - CollectResponse, - UploadFilesResponse, - CardMetadata, - InputStyles, - LabelStyles, - ErrorTextStyles, - RedactionType, - IRevealRecord as RevealRecord, - RevealResponse, - RenderFileResponse, - IValidationRule as ValidationRule, - ValidationRuleType, - EventName, - LogLevel, - Env, - ElementState, - ErrorType, - ErrorMessages, -} from './utils/common'; - -export { - IRevealElementInput as RevealElementInput, - IRevealElementOptions as RevealElementOptions, -} from './core/external/reveal/reveal-container'; - -export { ThreeDSBrowserDetails } from './core/external/threeds/threeds'; - -export { - CardType, - ElementType, -} from './core/constants'; - -export { - ContainerType, - ISkyflow as SkyflowConfig, -} from './skyflow'; - -export { default as CollectElement } from './core/external/collect/collect-element'; -export { default as CollectContainer } from './core/external/collect/collect-container'; -export { default as ComposableContainer } from './core/external/collect/compose-collect-container'; -export { default as ComposableElement } from './core/external/collect/compose-collect-element'; -export { default as RevealContainer } from './core/external/reveal/reveal-container'; -export { default as RevealElement } from './core/external/reveal/reveal-element'; -export { default as ThreeDS } from './core/external/threeds/threeds'; -export { default as ComposableRevealContainer } from './core/external/reveal/composable-reveal-container'; -export { default as ComposableRevealElement } from './core/external/reveal/composable-reveal-element'; -export default Skyflow; diff --git a/src/skyflow.ts b/src/skyflow.ts deleted file mode 100644 index 5585d290e..000000000 --- a/src/skyflow.ts +++ /dev/null @@ -1,388 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -import bus from 'framebus'; -import uuid from './libs/uuid'; -import { - ElementType, - ELEMENT_EVENTS_TO_IFRAME, - SDK_VERSION, - SESSION_ID, - CardType, -} from './core/constants'; -import Client from './client'; -import RevealContainer from './core/external/reveal/reveal-container'; -import CollectContainer from './core/external/collect/collect-container'; -import properties from './properties'; -import isTokenValid from './utils/jwt-utils'; -import SkyflowContainer from './core/external/skyflow-container'; -import { parameterizedString, printLog } from './utils/logs-helper'; -import SkyflowError from './libs/skyflow-error'; -import logs from './utils/logs'; -import SKYFLOW_ERROR_CODE from './utils/constants'; -import { - RequestMethod, - IInsertRecordInput, - IDetokenizeInput, - IGetInput, - RedactionType, - EventName, - Env, - LogLevel, - MessageType, - ValidationRuleType, - IGetByIdInput, - IInsertOptions, - IDeleteRecordInput, - IDeleteOptions, - IGetOptions, - InsertResponse, - GetResponse, - GetByIdResponse, - DeleteResponse, - ContainerOptions, - DetokenizeResponse, - IUpdateRequest, - UpdateResponse, - IUpdateOptions, - ErrorType, -} from './utils/common'; -import { formatVaultURL, checkAndSetForCustomUrl } from './utils/helpers'; -import ComposableContainer from './core/external/collect/compose-collect-container'; -import { validateComposableContainerOptions } from './utils/validators'; -import ThreeDS from './core/external/threeds/threeds'; -import { ClientMetadata, SkyflowElementProps } from './core/internal/internal-types'; -import ComposableRevealContainer from './core/external/reveal/composable-reveal-container'; - -export enum ContainerType { - COLLECT = 'COLLECT', - REVEAL = 'REVEAL', - COMPOSABLE = 'COMPOSABLE', - COMPOSE_REVEAL = 'COMPOSABLE_REVEAL', -} -export interface SkyflowConfigOptions { - logLevel?: LogLevel; - env?: Env; - trackingKey?: string; - trackMetrics?: boolean; - customElementsURL?: string; -} -export interface ISkyflow { - vaultID?: string; - vaultURL?: string; - getBearerToken: () => Promise; - options?: SkyflowConfigOptions; -} - -const CLASS_NAME = 'Skyflow'; -class Skyflow { - #client: Client; - - #uuid: string = uuid(); - - #metadata: ClientMetadata = { - uuid: this.#uuid, - clientDomain: window.location.origin, - }; - - #skyflowContainer: SkyflowContainer; - - #bearerToken: string = ''; - - #options: any; - - #logLevel:LogLevel; - - #env:Env; - - #skyflowElements: Array; - - constructor(config: ISkyflow) { - const localSDKversion = localStorage.getItem('sdk_version') || ''; - this.#metadata[SDK_VERSION] = localSDKversion; - this.#metadata[SESSION_ID] = uuid(); - this.#client = new Client( - { - ...config, - }, - this.#metadata, - ); - this.#logLevel = config?.options?.logLevel || LogLevel.ERROR; - this.#env = config?.options?.env || Env.PROD; - this.#skyflowElements = []; - this.#skyflowContainer = new SkyflowContainer(this.#client, - { logLevel: this.#logLevel, env: this.#env }); - - const cb = (data, callback: Function) => { - printLog(parameterizedString(logs.infoLogs.CAPTURED_BEARER_TOKEN_EVENT, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - if ( - this.#client.config.getBearerToken - && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) - ) { - this.#client.config - .getBearerToken() - .then((bearerToken) => { - if (isTokenValid(bearerToken)) { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - this.#bearerToken = bearerToken; - callback({ authToken: this.#bearerToken }); - } else { - printLog(parameterizedString( - logs.errorLogs.INVALID_BEARER_TOKEN, - ), MessageType.ERROR, this.#logLevel); - callback({ - error: parameterizedString( - logs.errorLogs.INVALID_BEARER_TOKEN, - ), - }); - } - }) - .catch((err) => { - printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, - this.#logLevel); - callback({ error: err }); - }); - } else { - printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - callback({ authToken: this.#bearerToken }); - } - }; - - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN + this.#uuid, cb); - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_LISTENER, CLASS_NAME), MessageType.LOG, - this.#logLevel); - printLog(parameterizedString(logs.infoLogs.CURRENT_ENV, CLASS_NAME, this.#env), - MessageType.LOG, this.#logLevel); - printLog(parameterizedString(logs.infoLogs.CURRENT_LOG_LEVEL, CLASS_NAME, this.#logLevel), - MessageType.LOG, this.#logLevel); - } - - static init(config: ISkyflow): Skyflow { - const logLevel = config?.options?.logLevel || LogLevel.ERROR; - checkAndSetForCustomUrl(config); - printLog(parameterizedString(logs.infoLogs.INITIALIZE_CLIENT, CLASS_NAME), MessageType.LOG, - logLevel); - - const tempConfig = config; - tempConfig.vaultURL = formatVaultURL(config.vaultURL); - const skyflow = new Skyflow(tempConfig); - printLog(parameterizedString(logs.infoLogs.CLIENT_INITIALIZED, CLASS_NAME), - MessageType.LOG, logLevel); - return skyflow; - } - - #getSkyflowBearerToken: () => Promise = () => new Promise((resolve, reject) => { - if ( - this.#client.config.getBearerToken - && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) - ) { - this.#client.config - .getBearerToken() - .then((bearerToken) => { - if (isTokenValid(bearerToken)) { - printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - this.#bearerToken = bearerToken; - resolve(this.#bearerToken); - } else { - printLog(parameterizedString( - logs.errorLogs.INVALID_BEARER_TOKEN, - ), MessageType.ERROR, this.#logLevel); - reject({ - error: parameterizedString( - logs.errorLogs.INVALID_BEARER_TOKEN, - ), - }); - } - }) - .catch((err) => { - printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, - this.#logLevel); - reject({ error: err }); - }); - } else { - printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - resolve(this.#bearerToken); - } - }); - - container(type: ContainerType.COLLECT, options?: ContainerOptions): CollectContainer; - container(type: ContainerType.COMPOSABLE, options?: ContainerOptions): ComposableContainer; - container(type: ContainerType.REVEAL, options?: ContainerOptions): RevealContainer; - container(type: ContainerType.COMPOSE_REVEAL, - options?: ContainerOptions) - : ComposableRevealContainer; - container(type: ContainerType, options?: ContainerOptions) { - switch (type) { - case ContainerType.COLLECT: { - const collectContainer = new CollectContainer({ - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - getSkyflowBearerToken: this.#getSkyflowBearerToken, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }, options); - printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - return collectContainer; - } - case ContainerType.REVEAL: { - const revealContainer = new RevealContainer({ - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - getSkyflowBearerToken: this.#getSkyflowBearerToken, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }, options); - printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - return revealContainer; - } - case ContainerType.COMPOSABLE: { - validateComposableContainerOptions(options!); - const composableContainer = new ComposableContainer({ - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - getSkyflowBearerToken: this.#getSkyflowBearerToken, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }, options!); - printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - return composableContainer; - } - - case ContainerType.COMPOSE_REVEAL: { - validateComposableContainerOptions(options!); - const revealComposableContainer = new ComposableRevealContainer({ - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - getSkyflowBearerToken: this.#getSkyflowBearerToken, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }, options); - printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), - MessageType.LOG, - this.#logLevel); - return revealComposableContainer; - } - - default: - if (!type) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_CONTAINER_TYPE, [], true); - } - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_CONTAINER_TYPE, [type], true); - } - } - - insert( - records: IInsertRecordInput, - options?: IInsertOptions, - ): Promise { - printLog(parameterizedString(logs.infoLogs.INSERT_TRIGGERED, CLASS_NAME), MessageType.LOG, - this.#logLevel); - return this.#skyflowContainer.insert(records, options); - } - - detokenize(detokenizeInput: IDetokenizeInput): Promise { - printLog(parameterizedString(logs.infoLogs.DETOKENIZE_TRIGGERED, CLASS_NAME), - MessageType.LOG, this.#logLevel); - return this.#skyflowContainer.detokenize(detokenizeInput); - } - - getById(getByIdInput: IGetByIdInput): Promise { - printLog(logs.warnLogs.GET_BY_ID_DEPRECATED, MessageType.WARN, this.#logLevel); - printLog(parameterizedString(logs.infoLogs.GET_BY_ID_TRIGGERED, CLASS_NAME), - MessageType.LOG, this.#logLevel); - return this.#skyflowContainer.getById(getByIdInput); - } - - get(getInput: IGetInput, options?: IGetOptions): Promise { - printLog(parameterizedString(logs.infoLogs.GET_TRIGGERED, CLASS_NAME), - MessageType.LOG, this.#logLevel); - return this.#skyflowContainer.get(getInput, options); - } - - delete(records: IDeleteRecordInput, options?: IDeleteOptions): Promise { - printLog(parameterizedString(logs.infoLogs.DELETE_TRIGGERED, CLASS_NAME), MessageType.LOG, - this.#logLevel); - return this.#skyflowContainer.delete(records, options); - } - - update(record: IUpdateRequest, options?: IUpdateOptions): Promise { - printLog(parameterizedString(logs.infoLogs.UPDATE_TRIGGERED, CLASS_NAME), MessageType.LOG, - this.#logLevel); - return this.#skyflowContainer.update(record, options); - } - - static get ContainerType() { - return ContainerType; - } - - static get ElementType() { - return ElementType; - } - - static get RedactionType() { - return RedactionType; - } - - static get ErrorType() { - return ErrorType; - } - - static get RequestMethod() { - return RequestMethod; - } - - static get LogLevel() { - return LogLevel; - } - - static get EventName() { - return EventName; - } - - static get Env() { - return Env; - } - - static get ValidationRuleType() { - return ValidationRuleType; - } - - static get CardType() { - return CardType; - } - - static get Error() { - return SkyflowError; - } - - static get ThreeDS() { - return ThreeDS; - } -} -export default Skyflow; diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts deleted file mode 100644 index eb6d78b6a..000000000 --- a/src/utils/common/index.ts +++ /dev/null @@ -1,411 +0,0 @@ -/* -Copyright (c) 2025 Skyflow, Inc. -*/ -import { IUpsertOptions } from '../../core-utils/collect'; -import { CardType, ElementType } from '../../core/constants'; - -declare global { - interface Window { - CoralogixRum: any; - } -} - -// export type ErrorKey = typeof ERROR_TYPE[keyof typeof ERROR_TYPE]; - -export enum ErrorType { - BAD_REQUEST = '400', - UNAUTHORIZED = '401', - FORBIDDEN = '403', - NOT_FOUND = '404', - TOO_MANY_REQUESTS = '429', - INTERNAL_SERVER_ERROR = '500', - BAD_GATEWAY = '502', - SERVICE_UNAVAILABLE = '503', - CONNECTION = 'CONNECTION', - TIMEOUT = 'TIMEOUT', - ABORT = 'ABORT', - NETWORK_GENERIC = 'NETWORK_GENERIC', - OFFLINE = 'OFFLINE', -} - -export type ErrorMessages = { - [key in ErrorType]: string; -}; - -export enum RedactionType { - DEFAULT = 'DEFAULT', - PLAIN_TEXT = 'PLAIN_TEXT', - MASKED = 'MASKED', - REDACTED = 'REDACTED', -} - -export enum RequestMethod { - GET = 'GET', - POST = 'POST', - PUT = 'PUT', - PATCH = 'PATCH', - DELETE = 'DELETE', -} - -export enum EventName { - CHANGE = 'CHANGE', - READY = 'READY', - FOCUS = 'FOCUS', - BLUR = 'BLUR', - SUBMIT = 'SUBMIT', -} - -export enum LogLevel{ - WARN = 'WARN', - INFO = 'INFO', - DEBUG = 'DEBUG', - ERROR = 'ERROR', -} - -export enum Env{ - DEV = 'DEV', - PROD = 'PROD', -} - -export enum MessageType{ - LOG = 'LOG', - WARN = 'WARN', - ERROR = 'ERROR', -} - -export enum ValidationRuleType { - REGEX_MATCH_RULE = 'REGEX_MATCH_RULE', - LENGTH_MATCH_RULE = 'LENGTH_MATCH_RULE', - ELEMENT_VALUE_MATCH_RULE = 'ELEMENT_VALUE_MATCH_RULE', -} - -export interface IInsertRecordInput { - records: IInsertRecord[]; -} - -export interface IInsertRecord { - table: string; - fields: Record; - skyflowID?: string; -} - -export interface IUpdateRequest { - table: string; - fields: Record; - skyflowID: string; -} - -export interface IUpdateOptions { - tokens?: boolean; -} - -export interface IRevealRecord { - token?: string; - redaction?: RedactionType; - column?: string; - skyflowID?: string; - table?: string; -} - -export interface IRevealRecordComposable { - token?: string; - redaction?: RedactionType; - column?: string; - skyflowID?: string; - table?: string; - iframeName?: string; -} - -export interface IInsertResponse { - records: IInsertResponseReocrds[]; -} -export interface IInsertResponseReocrds { - table: string; - fields?: Record; - skyflowID?: string; -} -export interface IRevealResponseType { - records?: Record[]; - errors?: Record[]; -} -export interface IRenderResponseType { - fields?: Record - errors?: Record - fileMetadata?: Record -} - -export interface IDetokenizeInput { - records: IRevealRecord[]; -} - -export interface IGetRecord { - ids?: string[]; - redaction?: RedactionType; - table: string; - columnName?:string; - columnValues?: string[]; -} - -export interface IGetInput { - records: IGetRecord[]; -} - -export interface IGetOptions { - tokens?: boolean; -} - -export interface ISkyflowIdRecord { - ids: string[]; - redaction: RedactionType; - table: string; -} - -export interface IGetByIdInput { - records: ISkyflowIdRecord[]; -} - -export interface Context{ - logLevel:LogLevel - env:Env -} - -export interface IValidationRule { - type: ValidationRuleType; - params: any; -} - -export interface IUpsertOption { - table : string; - column: string; -} - -export interface IInsertOptions{ - tokens?: boolean; - upsert?: IUpsertOption[]; -} - -export interface IDeleteRecord { - id: String; - table: String; -} - -export interface IDeleteOptions {} - -export interface IDeleteRecordInput { - // options?: IDeleteOptions; - records: IDeleteRecord[]; -} - -export interface IDeleteResponseType { - records?: Record[]; - errors?: Record[]; -} - -export interface MeticsObjectType { - element_id: string, - element_type: string[], - div_id: string, - container_id: string, - container_name: string, - session_id: string, - vault_id: string, - vault_url: string, - events: string[], - created_at: number, - region: string, - mount_start_time?: number, - mount_end_time?: number, - error?: string, - latency?: number, - status: 'SUCCESS' | 'INITIALIZED' | 'PARTIAL_RENDER' | 'FAILED' | string, - sdk_name_version: string, - sdk_client_device_model: string | undefined, - sdk_client_os_details: string, - sdk_runtime_details: string -} - -export interface SharedMeticsObjectType { - records: MeticsObjectType[]; -} - -export interface InsertResponse { - records?: InsertResponseRecords[], - errors?: ErrorRecord[], -} - -export interface UpdateResponseType { - skyflowID: string; - [key: string]: unknown; -} - -export interface UpdateResponse { - updatedField: UpdateResponseType -} - -export interface CollectResponse extends InsertResponse {} -export interface DetokenizeRecord extends IRevealRecord {} -export interface DetokenizeResponse extends IRevealResponseType {} - -export interface InsertResponseRecords { - fields: Record, - table: string, - skyflow_id?: string, -} - -export interface ErrorRecord { - code: number, - description: string, -} - -export interface GetByIdResponse { - records?: GetByIdResponseRecord[], - errors?: ErrorRecord[], -} - -export interface GetByIdResponseRecord { - fields: Record, - table: string, -} - -export interface GetResponse { - records?: GetResponseRecord[], - errors?: ErrorRecord[], -} - -export interface GetResponseRecord { - fields: Record, - table: string, -} - -export interface DeleteResponse { - records?: DeleteResponseRecord[], - errors?: DeleteErrorRecords[], -} -export interface DeleteResponseRecord { - skyflow_id: string, - deleted: boolean, -} - -export interface DeleteErrorRecords { - id: string, - error: ErrorRecord, -} - -export type Style = string | { [key: string]: Style }; - -export interface ContainerOptions { - layout: number[], - styles?: InputStyles, // check implementation below - errorTextStyles?: ErrorTextStyles, // check implementation below -} - -export interface ErrorTextStyles { - base?: Record, - global?: Record, -} - -export interface LabelStyles extends ErrorTextStyles { - focus?: Record, - requiredAsterisk?: Record, -} - -export interface InputStyles extends ErrorTextStyles { - focus?: Record, - complete?: Record, - empty?: Record, - invalid?: Record, - cardIcon?: Record, - copyIcon?: Record, -} - -export interface CollectElementOptions { - required?: boolean, - format?: string, - translation?: Record, - enableCardIcon?: boolean, - enableCopy?: boolean, - cardMetadata?: CardMetadata, - preserveFileName?: boolean, - allowedFileType?: string[], - blockEmptyFiles?: boolean, - maxFileSize?: number, - maxFileCount?: number, - masking?: boolean, - maskingChar?: string, -} - -export interface CardMetadata { - scheme?: CardType[], -} - -interface CollectElementCommonProps { - table?: string, - column?: string, - label?: string, - inputStyles?: InputStyles, - labelStyles?: LabelStyles, - errorTextStyles?: ErrorTextStyles, - placeholder?: string, - altText?: string, - validations?: IValidationRule[], - skyflowID?: string, -} - -export interface CollectElementUpdateOptions extends CollectElementCommonProps { - cardMetadata?: CardMetadata, -} - -export interface CollectElementInput extends CollectElementCommonProps { - type: ElementType, -} - -export interface ICollectOptions { - tokens?: boolean, - additionalFields?: IInsertRecordInput, - upsert?: Array, -} -export interface MetaData { - [key: string]: any, -} -export interface EventConfig{ - authToken: string, - vaultURL: string, - vaultID: string, -} - -export interface UploadFilesResponse { - fileUploadResponse?: Record, - errorResponse?: Record, -} - -export interface RevealResponse { - success?: Array<{ - token: string, - valueType: string, - }>, - errors?: Array<{ - error: ErrorRecord, - token: string, - }> -} - -export interface RenderFileResponse { - success?: { - skyflow_id: string, - column: string, - }, - errors?: { - skyflowId: string, - error: ErrorRecord, - column: string, - }, -} - -export interface ElementState { - isEmpty: boolean, - isValid: boolean, - isFocused: boolean, - value: string | Object | Blob | undefined, - isRequired: boolean, - selectedCardScheme?: string, -} diff --git a/tsconfig.json b/tsconfig.base.json similarity index 79% rename from tsconfig.json rename to tsconfig.base.json index 21cfb8f90..f1eed5159 100644 --- a/tsconfig.json +++ b/tsconfig.base.json @@ -17,8 +17,9 @@ "isolatedModules": false, "noEmit": false, "declaration": true, - "outDir": "types" - }, - "include": ["src","src/custom.d.ts","typings.d.ts"], - "exclude": ["node_modules", "dist", "tests/*.test.*"] + "baseUrl": ".", + "paths": { + "@core/*": ["core/*"] + } + } } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 000000000..3d1f9e1de --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "include": [ + "core", + "packages/skyflow-js/src", + "packages/skyflow-js/src/custom.d.ts", + "packages/skyflow-js/typings.d.ts", + "packages/skyflow-flowvault-js/src", + "packages/skyflow-flowvault-js/src/custom.d.ts", + "packages/skyflow-flowvault-js/typings.d.ts" + ] +} diff --git a/typings.d.ts b/typings.d.ts deleted file mode 100644 index fe3595b18..000000000 --- a/typings.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* -Copyright (c) 2023 Skyflow, Inc. -*/ -declare module '*.json'; diff --git a/webpack.common.js b/webpack.common.js deleted file mode 100644 index 473cd64c2..000000000 --- a/webpack.common.js +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright (c) 2022 Skyflow, Inc. -*/ -const webpack = require('webpack'); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); -const NodePolyfillPlugin = require("node-polyfill-webpack-plugin") - -module.exports = { - target: 'web', - resolve: { - extensions: ['.ts', '.js', '.json'], - }, - module: { - rules: [ - { test: /\.(ts|js)x?$/, loader: 'babel-loader', exclude: /node_modules/ }, - { - test:/\.svg$/, - type:'asset/resource' - } - ], - }, - - plugins: [ - new ForkTsCheckerWebpackPlugin(), - new NodePolyfillPlugin(), - new webpack.DefinePlugin({ - 'process.env': JSON.stringify({ - IFRAME_SECURE_SITE: process.env.IFRAME_SECURE_SITE, - IFRAME_SECURE_ORIGIN: process.env.IFRAME_SECURE_ORIGIN, - }), - }), - ], -}; diff --git a/webpack.skyflow-browser.js b/webpack/browser.js similarity index 64% rename from webpack.skyflow-browser.js rename to webpack/browser.js index bfb9edea2..f2c9fc22c 100644 --- a/webpack.skyflow-browser.js +++ b/webpack/browser.js @@ -1,24 +1,27 @@ /* -Copyright (c) 2022 Skyflow, Inc. +Copyright (c) 2025 Skyflow, Inc. */ +// Shared production browser-SDK build factory. A package's +// webpack.skyflow-browser.js is a one-line wrapper: +// module.exports = require('../../webpack/browser.js')(__dirname); const { merge } = require('webpack-merge'); const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const terserWebpackPlugin = require('terser-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); -const common = require('./webpack.common.js'); +const common = require('./common.js'); -module.exports = () => merge(common, { +module.exports = (packageDir) => () => merge(common(packageDir), { mode: 'production', entry: { - index: [ path.resolve(__dirname, 'src/index.ts')], + index: [path.resolve(packageDir, 'src/index.ts')], }, output: { filename: '[name].js', - path: path.resolve(__dirname, 'dist/v1'), + path: path.resolve(packageDir, 'dist/v1'), }, optimization: { // splitChunks: { diff --git a/webpack/common.js b/webpack/common.js new file mode 100644 index 000000000..66dc5825f --- /dev/null +++ b/webpack/common.js @@ -0,0 +1,61 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Shared webpack "common" factory for every SDK package (skyflow-js, +// skyflow-flowvault-js, ...). Each package's build wrappers call this with their +// own package directory so the loaders, the `@core` alias and the DefinePlugin +// wiring are defined exactly once, while telemetry identity stays per-package. +const path = require('path'); +const webpack = require('webpack'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); + +module.exports = (packageDir) => { + // SDK telemetry identity — injected per package at build time from the CALLING + // package's own package.json (each package injects its own name/version, so + // telemetry + the JS/React language-label resolve to that package's identity). + const pkg = require(path.join(packageDir, 'package.json')); + + return { + target: 'web', + resolve: { + extensions: ['.ts', '.js', '.json'], + // `@core` — mirror of the tsconfig.base.json path alias (single source of + // truth). Keep this in sync with tsconfig.base.json `paths` and + // jest.config.json `moduleNameMapper`. core/ lives at the monorepo root, + // one level up from this webpack/ folder. + alias: { + '@core': path.resolve(__dirname, '../core'), + }, + }, + module: { + rules: [ + // rootMode:'upward' so files under core/ (outside each package) resolve + // the single shared babel.config.js at the monorepo root. + { + test: /\.(ts|js)x?$/, + loader: 'babel-loader', + exclude: /node_modules/, + options: { rootMode: 'upward' }, + }, + { + test: /\.svg$/, + type: 'asset/resource', + }, + ], + }, + + plugins: [ + new ForkTsCheckerWebpackPlugin(), + new NodePolyfillPlugin(), + new webpack.DefinePlugin({ + 'process.env': JSON.stringify({ + IFRAME_SECURE_SITE: process.env.IFRAME_SECURE_SITE, + IFRAME_SECURE_ORIGIN: process.env.IFRAME_SECURE_ORIGIN, + }), + SDK_NAME: JSON.stringify(pkg.name), + SDK_VERSION: JSON.stringify(pkg.version), + }), + ], + }; +}; diff --git a/webpack.dev.js b/webpack/dev.js similarity index 53% rename from webpack.dev.js rename to webpack/dev.js index 0ae28454a..e93e779c1 100644 --- a/webpack.dev.js +++ b/webpack/dev.js @@ -1,11 +1,19 @@ /* -Copyright (c) 2022 Skyflow, Inc. +Copyright (c) 2025 Skyflow, Inc. */ +// Shared dev-server build factory (webpack serve). Per-package knobs — the dev +// server port, the bundle-analyzer port, and an optional local `/vault` proxy — +// are passed in so two SDK dev servers can run side by side. A package's +// webpack.dev.js is a thin wrapper: +// module.exports = require('../../webpack/dev.js')(__dirname, { +// port: 3040, analyzerPort: 8881, +// // proxy: { '/vault': { target: 'https://', ... } }, // local only +// }); const path = require('path'); const { merge } = require('webpack-merge'); -const common = require('./webpack.common.js'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const BundleAnalyser = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const common = require('./common.js'); const minify = { collapseWhitespace: true, @@ -18,22 +26,25 @@ const minify = { minifyJS: true, }; -module.exports = () => merge(common, { +module.exports = (packageDir, { port, analyzerPort, proxy } = {}) => () => merge(common(packageDir), { entry: { - skyflow: [ path.resolve(__dirname, 'src/index.ts')], + skyflow: [path.resolve(packageDir, 'src/index.ts')], iframe: [ - path.resolve(__dirname, 'src/index-internal.ts'), + path.resolve(packageDir, 'src/index-internal.ts'), ], }, output: { filename: '[name].js', - path: path.resolve(__dirname, 'dist'), + path: path.resolve(packageDir, 'dist'), }, mode: 'development', devtool: 'inline-source-map', devServer: { - port: 3040, + port, + // Optional per-developer proxy (e.g. `/vault` -> a dev vault). Kept out of + // the shared factory so proxy targets never get committed. + ...(proxy ? { proxy } : {}), historyApiFallback: true, open: true, // todo: add routes for iframe and index ex: / for index.html and iframe for iframe.html @@ -49,16 +60,16 @@ module.exports = () => merge(common, { }, }, plugins: [ - new BundleAnalyser({ analyzerPort: 8881 }), + new BundleAnalyser({ analyzerPort }), new HtmlWebpackPlugin({ - template: 'assets/index.html', + template: path.resolve(__dirname, '../assets/index.html'), chunks: ['skyflow'], inject: 'head', minify, }), new HtmlWebpackPlugin({ filename: 'iframe.html', - template: 'assets/iframe.html', + template: path.resolve(__dirname, '../assets/iframe.html'), chunks: ['iframe'], inject: 'head', minify, diff --git a/webpack.iframe.js b/webpack/iframe.js similarity index 70% rename from webpack.iframe.js rename to webpack/iframe.js index cc0ee2dd2..c727d03f1 100644 --- a/webpack.iframe.js +++ b/webpack/iframe.js @@ -1,6 +1,9 @@ /* -Copyright (c) 2022 Skyflow, Inc. +Copyright (c) 2025 Skyflow, Inc. */ +// Shared production iframe (secure elements) build factory. A package's +// webpack.iframe.js is a one-line wrapper: +// module.exports = require('../../webpack/iframe.js')(__dirname); const { merge } = require('webpack-merge'); const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); @@ -8,7 +11,7 @@ const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const terserWebpackPlugin = require('terser-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const HtmlWebPackPlugin = require('html-webpack-plugin'); -const common = require('./webpack.common.js'); +const common = require('./common.js'); const minify = { collapseWhitespace: true, @@ -21,18 +24,18 @@ const minify = { minifyJS: true, }; -module.exports = () => merge(common, { +module.exports = (packageDir) => () => merge(common(packageDir), { mode: 'production', entry: { index: [ - path.resolve(__dirname, 'src/index-internal.ts'), + path.resolve(packageDir, 'src/index-internal.ts'), ], }, output: { filename: '[name].js', - path: path.resolve(__dirname, 'dist/v1/elements'), + path: path.resolve(packageDir, 'dist/v1/elements'), }, optimization: { @@ -47,7 +50,7 @@ module.exports = () => merge(common, { plugins: [ new HtmlWebPackPlugin({ filename: 'index.html', - template: 'assets/iframe.html', + template: path.resolve(__dirname, '../assets/iframe.html'), chunks: ['index'], inject: 'head', minify, diff --git a/webpack.skyflow-node.js b/webpack/node.js similarity index 60% rename from webpack.skyflow-node.js rename to webpack/node.js index 6dfa2bbac..5e382e3cd 100644 --- a/webpack.skyflow-node.js +++ b/webpack/node.js @@ -1,23 +1,26 @@ /* -Copyright (c) 2022 Skyflow, Inc. +Copyright (c) 2025 Skyflow, Inc. */ +// Shared production node-SDK (UMD) build factory. The UMD global differs per +// package, so it is passed in. A package's webpack.skyflow-node.js is: +// module.exports = require('../../webpack/node.js')(__dirname, { library: 'Skyflow' }); const { merge } = require('webpack-merge'); const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const terserWebpackPlugin = require('terser-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); -const common = require('./webpack.common.js'); +const common = require('./common.js'); -module.exports = () => merge(common, { +module.exports = (packageDir, { library }) => () => merge(common(packageDir), { mode: 'production', entry: { - index: [ path.resolve(__dirname, 'src/index-node.ts')], + index: [path.resolve(packageDir, 'src/index-node.ts')], }, output: { filename: '[name].js', - path: path.resolve(__dirname, 'dist/sdkNodeBuild'), - library: 'Skyflow', + path: path.resolve(packageDir, 'dist/sdkNodeBuild'), + library, libraryTarget: 'umd', globalObject: 'this', umdNamedDefine: true,