From cf58d29935947bcaf4648ddeed5a786998f9eadf Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Thu, 6 Aug 2026 13:46:03 +0530 Subject: [PATCH 001/103] SK-3041:Add plan documents for package split. --- docs/package-split-plan.md | 302 +++++++++++++++++++++++++++++++++ docs/phase-1-execution-plan.md | 201 ++++++++++++++++++++++ docs/phase-2-execution-plan.md | 180 ++++++++++++++++++++ docs/phase-3-execution-plan.md | 169 ++++++++++++++++++ 4 files changed, 852 insertions(+) create mode 100644 docs/package-split-plan.md create mode 100644 docs/phase-1-execution-plan.md create mode 100644 docs/phase-2-execution-plan.md create mode 100644 docs/phase-3-execution-plan.md diff --git a/docs/package-split-plan.md b/docs/package-split-plan.md new file mode 100644 index 00000000..01b59931 --- /dev/null +++ b/docs/package-split-plan.md @@ -0,0 +1,302 @@ +# Splitting `skyflow-js` into privacyDB + flowDB Packages — Architecture & Migration Plan + +**Status:** Proposed +**Scope:** Split the single `skyflow-js` codebase into two independently publishable npm packages that share one common core, with flowDB built by *extending* common interfaces. +**Sources:** privacyDB baseline restored from `main`; flowDB deltas taken from the `2.9.0-beta.1` tag. + +--- + +## 1. Goal + +Produce two independently versioned, independently published npm packages that share a single common code + interface layer: + +| Unit | Surface | Sourced from | First version | +|---|---|---|---| +| `skyflow-js` (package) | **privacyDB** (existing public API) | `main` | continues current semver | +| `skyflow-flowvault-js` (package) | **flowDB** (Flow Vault `/v2` API) | `2.9.0-beta.1` tag deltas | `1.0.0` | +| `core/` (shared **folder**, not a package) | shared code + interfaces | common ancestor of both | n/a — compiled into each package | + +The organizing principle: **create common code & interfaces both SDKs use, and implement flowDB by extending those common things** — not by branching inside shared code. There is no runtime `isFlowDB` toggle in the target design; the variant is fixed per package at build time. + +The shared code lives in a plain `core/` **folder** (not an npm package): both SDK packages import it via a `@core` path alias, it is compiled into each package's bundle at build time, and a boundary lint rule keeps it variant-neutral (see §3.1). Only the two SDKs are packages, because only they are published. + +This reframes the split as a **three-way factoring of a common ancestor**: + +- `core/` ≈ infra present nearly identically in both `main` and the `2.9.0-beta.1` flowDB tag. +- `skyflow-js` ≈ `main` minus core. +- `skyflow-flowvault-js` ≈ the `2.9.0-beta.1` flowDB additions minus core, expressed as core extensions. + +--- + +## 2. Key findings that shape the plan + +1. **The npm tarball is small; the expensive shared runtime is already externalized.** `package.json` ships only `dist/sdkNodeBuild` (UMD node bundle) + `types/`. The browser bundle (`dist/v1`) and the **entire Elements iframe runtime** (`dist/v1/elements`, built from `src/index-internal.ts`) deploy to S3/CloudFront and load at runtime via `IFRAME_SECURE_SITE` / `customElementsURL`. The largest shared asset never enters the package boundary. + +2. **The data layer already uses a variant-adapter pattern.** `core-utils/collect.ts` and `reveal.ts` are structured around `IInsertVariant` / `IDetokenizeVariant` strategy objects (`flowDBInsertVariant`, `flowDBDetokenizeVariant`) fed into a shared `executeInsert(variant, …)` / `executeDetokenize(variant, …)`. flowDB's adapter is built; privacyDB's is restored from `main`. + +3. **There is no runtime variant flag anywhere.** Selection today is authoring-time: in `core-utils`, by which wrapper the caller imports (`insertDataInCollect` vs `insertDataInCollectFlowDB`); everywhere else the code unconditionally calls the flowDB wrapper. Per-package builds make this compile-time-fixed, which is cleaner. + +4. **The flowDB source is not a clean privacyDB baseline.** In `2.9.0-beta.1` the privacyDB *element* paths were replaced by flowDB, legacy privacyDB type exports are commented out, and many privacyDB tests are `skip`ped. Because `skyflow-js` is sourced from `main`, this is handled by construction rather than manual restoration. + +5. **`webpack.common.js` is package-agnostic.** No package name, output dir, or iframe URL is hardcoded in it; only `entry`, `output.path`, and the UMD `library` name differ per artifact. `webpack.dev.js` already runs multiple entry points through one config. + +--- + +## 3. Target architecture + +``` +repo/ + core/ # SHARED SOURCE — plain folder, NOT a package. Imported via @core alias, + # compiled into each package's bundle at build time. + - iframe skeleton + variant-neutral frame leaf-helpers + - event bus (event-emitter), framebus wrapper (libs/bus), bus-events + - iframer, JSS (jss-styles), metrics + - pure validators (card / Luhn / regex / format), validateElements, getUnformattedValue + - logs + error-codes (utils/constants), logs-helper, jwt-utils + - constants (event protocol, styles, card infra, element metadata, ElementType/CardType) + - neutral common types + BASE element-input / response-envelope interfaces + - skyflow-error BASE class + - uuid / regex / deep-clone + - index.ts barrel — the surface both SDKs consume + + packages/ + skyflow-js/ # privacyDB (from main). name: "skyflow-js" + - index.ts / index-node.ts (PDB public surface, incl. /v1 response types) + - index-internal.ts → builds PDB iframe → hosted at PDB URL + - /v1 data layer (privacyDB collect/reveal/get/getById/delete builders) + - PDB element input/response types extending @core bases + - PDB upsert options { table, column } + - PDB frame controller — owns its own tokenize/revealData, calls @core leaf helpers + + skyflow-flowvault-js/ # flowDB (from 2.9.0-beta.1 tag). name: "skyflow-flowvault-js", v1.0.0 + - index.ts / index-node.ts (flowDB public surface) + - index-internal.ts → builds flowvault iframe → hosted at flowvault URL + - /v2 data layer (flowDB insert/update/detokenize builders + parsers) + - flowDB element input/response types extending @core bases + - flowDB upsert options { tableName, uniqueColumns, updateType } + - skyflow-flowdb-error (extends @core error base) + - flowDB frame controller — owns its own tokenize/revealData (incl. cvvMap), calls @core leaf helpers + + tsconfig.base.json # @core/* → core/* (single source of truth for the alias) + .eslintrc.js # boundary rule: core/ may not import from packages/* +``` + +Each SDK package builds all three artifacts (browser, node, **its own** iframe) from `@core + its variant`. `core/` is a shared source folder, not a package: it is compiled into each package's bundle at build time (nothing is published or installed separately, so consumers still install one self-contained package with no peer dependency). + +### 3.1 Packaging strategy: monorepo with workspaces + +Chosen over the alternatives: + +| Option | Bundle-size risk for existing skyflow-js | Sync burden | Verdict | +|---|---|---|---| +| **Monorepo + workspaces (chosen)** | None — each SDK bundles only its variant + core | Low — one source of truth, atomic cross-cutting PRs | ✅ | +| Single build, 2 entry points, tree-shake | High/uncertain — interleaved `core-utils` can't tree-shake apart; UMD/IIFE tree-shakes poorly | Low | ✗ doesn't deliver two publishable packages | +| Two repos + shared scoped package | None | High — shared code drifts, cross-repo PRs | ✗ sync cost during fast-moving flowDB beta | + +**Versioning:** independent semver per SDK (`skyflow-js` continues its line; `skyflow-flowvault-js` starts at `1.0.0`). The `core/` folder has no version of its own — it is source compiled into each package. Same npm dist-tag scheme across both SDKs. + +**Shared core is a folder, not a package.** Only the two publishable SDKs are packages (each needs its own `package.json` `name`/`version`). The shared code is a plain `core/` folder consumed by both via a `@core` path alias, with two guardrails: + +- **Path alias (ergonomics)** — one source of truth in `tsconfig.base.json` `paths` (`@core/* → core/*`), mirrored to `webpack` `resolve.alias` and `jest` `moduleNameMapper`. Keeps imports clean (`@core/validators`, not `../../../core/...`) and relocation-safe. Because `core/` is not under `node_modules`, `babel-loader`'s `exclude: /node_modules/` does not skip it — it compiles normally, so **no build-ordering step and no separate core build.** +- **Boundary rule (enforcement)** — `import/no-restricted-paths` (`eslint-plugin-import`) forbids `core/**` from importing `packages/**`, so core stays variant-neutral. This is the actual enforcement; the alias does not enforce. (A folder relies on this lint rule where a package would add only a partial resolution-level barrier — which would still need the same rule for relative reach-arounds — so the folder loses no practical enforcement.) + +The two SDKs still live under npm **workspaces** purely for dev ergonomics (one `npm install`, shared devDeps, unified lint/test) — the shared *code* is just not one of the workspace packages. + +### 3.2 Iframe strategy: separate builds, single codebase + +**Separate iframe *artifacts*, single iframe *codebase*.** Each package builds its own iframe from the shared core skeleton + its own variant data layer, and hosts it at its own URL. + +Rationale: + +- The variant becomes **compile-time fixed** per iframe — no runtime adapter/branch inside the iframe. This *deletes* the hardest refactor item (injecting a data adapter into a shared controller). +- A flowDB data-layer change **cannot** break the privacyDB iframe — they are different artifacts. +- Each iframe bundle is smaller (one variant's builders). +- **Version-skew isolation:** each package owns both its client SDK and its hosted iframe, versioned together. A shared iframe would instead have to stay compatible with two independently-versioned SDKs at once — a worse compatibility surface. + +**How the iframe URL reaches the client (existing mechanism, replicated per package).** `properties.ts` reads `process.env.IFRAME_SECURE_ORIGIN`/`IFRAME_SECURE_SITE`; [webpack.common.js](../webpack.common.js) `DefinePlugin` inlines them at build time; the release workflow supplies them as `IFRAME_SECURE_SITE: "v${RELEASE_VERSION}/${secret}"` + `IFRAME_SECURE_ORIGIN: ${secret}`. So each build bakes an absolute URL `${ORIGIN}/v${version}/${SITE}` into the bundle; `getIframeSrc()` returns it, and `customElementsURL` can still override at runtime. To produce the two builds, each package's release workflow supplies its own origin/site secrets + version: + +| Package | `IFRAME_SECURE_ORIGIN` | `IFRAME_SECURE_SITE` | Baked URL (example) | +|---|---|---|---| +| `skyflow-js` | `https://js.skyflow.dev` | `elements/index.html` | `js.skyflow.dev/v2.7.9/elements/index.html` | +| `skyflow-flowvault-js` | `https://js-flowvault.skyflow.dev` | `elements/index.html` | `js-flowvault.skyflow.dev/v1.0.0/elements/index.html` | + +Same S3 bucket / different sub-root folder is fine — the bundle only cares about the final absolute URL. The two distinct **origins** are also what framebus uses for `postMessage` targeting (`.target(IFRAME_SECURE_ORIGIN)`), so separate hosts strengthen cross-package isolation. **Guardrail:** keep the committed `process.env`-based `properties.ts`; the working-tree localhost hardcode is local-dev only and must never be committed or it breaks prod iframe loading. Infra to-do (not SDK): map the `js-flowvault.skyflow.dev` host (CloudFront/CNAME) to its S3 sub-folder. + +Costs (accepted, low): + +- Two S3 uploads + two CloudFront distributions/invalidations — CI config, not code. +- Shared core code is duplicated inside both hosted iframe bundles — CDN assets, not user installs; a cache/bandwidth non-issue. +- The two `index-internal.ts` entries must stay structurally parallel — mitigated by core owning the skeleton so the entries are thin. + +**Non-negotiable:** "separate builds" must not become "fork the iframe code." ~90% of the iframe runtime (DOM building, element rendering, JSS, framebus protocol, frame lifecycle/height/ready handshake) is variant-neutral and lives once in `core/`. Core exposes that skeleton plus variant-neutral leaf helpers; each package composes them into its own frame controller (see §3.4 — helpers, not a callback base). + +### 3.3 Element inputs & responses: separate types, common rendering + +Three-way split: + +1. **Element rendering / DOM / event protocol → common (core).** Framebus event *names* (`COLLECT_CALL_REQUESTS`, `REVEAL_CALL_REQUESTS`, `COMPOSABLE_REVEAL`, etc.) live in core constants; variant-neutral leaf helpers — frame/element lookup, `getUnformattedValue`, checkbox concatenation, duplicate-element detection, and the element **validation pass** — become core helpers both packages call. (Note: `cvvMap` / mock-CVV token masking is **flowDB-only**, not a core concern.) + +2. **Element input & response *types* → separate, extending common bases.** They genuinely diverge: + - Reveal input: PDB `IRevealElementInput` (`skyflowID/table/column/redaction`) vs flowDB `IFlowDBRevealElementInput` (token-only + `tokenGroupRedactions`). + - Response envelope: flowDB unified `records[]` (`{tableName, skyflowId, tokens, hashedData, httpCode, error}`) vs PDB per-operation responses. + - Upsert options: PDB `{table, column}` vs flowDB `{tableName, uniqueColumns, updateType}` — confirmed different meanings. + + Core defines **base interfaces** for shared element/style fields and a base record/response envelope; each package **extends** them. Public names (`RevealElementInput`, `CollectResponse`, `UpsertOptions`, `SkyflowError`) then resolve to the correct shape per package automatically. + +3. **Parse/build logic for those structures → per-package** (in each package's data layer, and therefore in each package's iframe build): `construct*Request`, `formatRecordsForClient*`, `constructFlowDB*Response` are variant-specific and stay out of core. + +### 3.4 Frame-controller decomposition + +Applying §3.2/§3.3 to the frame controllers concretely: **core exposes only variant-neutral leaf helpers; each package owns its own `tokenize()` / `revealData()` end-to-end.** No template-method base that calls back into subclass hooks — that callback inversion is the "complex coupling with core" to avoid. Once `cvvMap` (flowDB-only), request keys, and the response envelope all differ per variant, the assembly loop is **genuinely different code, not duplication**; only the frame-iteration + validation pass is structurally identical, and it becomes one shared helper. + +Split of `tokenize()` ([skyflow-frame-controller.ts:610-813](../src/core/internal/skyflow-frame/skyflow-frame-controller.ts#L610-L813)): + +| Region | Lines | Owner | +|---|---|---| +| Validate elements | 617-651 | **core** — `validateElements(options, ctx) → errorMessage` helper (touches no request/response/cvv shape) | +| Assemble insert/update buckets | 653-743 | **per package** — keys differ; `cvvMap` branches are flowDB-only | +| Build request | 744-757 | **per package** — `constructFlowDBInsertRequest` etc. | +| Send + parse response | 763-806 | **per package** — `cvvMap` masking + response envelope are variant-specific | + +Core additionally supplies leaf primitives both packages call: frame/element lookup, `getUnformattedValue`, checkbox concatenation, duplicate-element detection. The same split applies to the second `tokenize()` in [frame-element-init.ts:299-504](../src/core/internal/frame-element-init.ts#L299-L504) and to `revealData`. Net: loose coupling **and** near-zero true duplication (only the validation pass is shared, via a plain function call — no inheritance). + +--- + +## 4. Coupling map (current single tree) + +Classification of the interleaved modules, for reference during extraction: + +| File | Nature | Action | +|---|---|---| +| `core-utils/collect.ts` | PDB + flowDB builders interleaved; `constructElementsInsertReq` + `replaceCVVTokensInResponse` (CVV is flowDB-only) mixed with variant transport | **Cut** — request/response builders per package; validators to core | +| `core-utils/reveal.ts` | 3 seams: flowDB detokenize/reveal, PDB `/v1` GET/getById/render-file, formatters | **Cut** (hardest file) — parse/format per package | +| `skyflow-frame-controller.ts` | one iframe brain for `/v1` pure-JS + flowDB elements; `tokenize()` = neutral validation + variant assembly/request/response | Each package owns its own `tokenize()`; core supplies leaf helpers only (see §3.4) | +| `frame-element-init.ts` | flowDB-only composable collect; neutral validation + variant assembly/request | Each package owns its own `tokenize()`; core leaf helpers only | +| `composable-frame-element-init.ts` | flowDB-only reveal | Move to flowvault | +| `internal-types/index.ts` | neutral internal types + ~25 flowDB interfaces + `CollectResponse`/`RevealResponse` | **Split** (neutral → core, flowDB → flowvault) | +| `libs/skyflow-flowdb-error.ts` | standalone flowDB error class (currently the public `SkyflowError`) | Move to flowvault; extend core error base | +| `libs/skyflow-error.ts` | neutral error base | Move to core | +| `collect-container.ts`, `reveal-container.ts`, `composable-reveal-container.ts` | variant-neutral except `SkyflowFlowDBError` import + `IFlowDBRevealElementInput` type | Container logic → core base; error/type per package | +| `skyflow.ts` | one `Skyflow` class + `container()` factory serves both | Core base + per-package entry | +| `properties.ts` | plain constants (iframe URL defaults) | Per-package (each sets its own iframe URL default) | +| `utils/common/index.ts` | neutral types **+** one flowDB import (`IFlowDBUpsertOptions`) | Neutral → core; invert the flowDB back-edge | +| `index-node.ts` | public names aliased onto flowDB impls | Per-package index | + +**Layering back-edges to invert (all type-only, mechanical) before extraction:** + +1. `utils/common/index.ts:4` → `IFlowDBUpsertOptions` from `core-utils/collect` (flowDB name in shared `ICollectOptions`). Move upsert-options types into core; make `common` variant-free. +2. `utils/helpers` & `utils/validators` → `reveal-container` + `skyflow.ts` input types. Relocate referenced interfaces into core types. +3. `libs/element-options` → `collect-element` / `compose-collect-element`. +4. `internal-types` → the `index-node` barrel + `external/skyflow-container`. +5. **The structural edge:** the four frame controllers hard-import flowDB builders from `core-utils`. Resolved by moving variant-neutral leaf helpers to `core/` and letting each package own its own controller (§3.4). + +**Cleanly shared today (no back-edges, no flowDB) → core as-is:** `event-emitter`, `libs/bus`, `bus-events`, `iframer`, `jss-styles`, `utils/logs`, `logs-helper`, `utils/constants` (error codes), `jwt-utils`, `libs/skyflow-error`, `uuid`/`regex`/`deep-clone`, `metrics`, `core/constants`, and the neutral bulk of `utils/common`. + +--- + +## 5. Migration phases (sequencing) + +### Phase 0 — Baseline +- Confirm the `main` privacyDB baseline and the `2.9.0-beta.1` flowDB delta set as the two source inputs. + +### Phase 1 — Establish `core/` from `main` (privacyDB only) — critical path +On an integration branch cut from `main`, extract the variant-neutral code into `core/` (behind the `@core` alias + boundary lint) and leave the existing `skyflow-js` (privacyDB) working on top of it. **No flowDB and no second package yet** — `main` has no flowDB to carve, and flowvault can't extend a `core/` that doesn't exist. Pure internal refactor: public surface + telemetry unchanged throughout. +- Scaffold `core/` + `@core` alias (tsconfig/webpack/jest) + `import/no-restricted-paths` (`core/` ⇏ `src/`). +- Move neutral leaves → `core/` (uuid/regex/bus/jss/logs/constants/iframer/metrics/jwt), then telemetry-identity injection, then neutral common types + **base interfaces**. +- Invert the `main` back-edges (`validators`/`helpers`/`element-options`/`internal-types` → container/`skyflow.ts` types) by relocating the referenced interfaces into `core/types`. +- Extract the variant-neutral **leaf helpers** (`validateElements`, `getUnformattedValue`, frame/element lookup, checkbox concat, duplicate detection) into `core/`; keep the pure-JS transport neutral. **No template-method base with subclass callbacks** (see §3.4). +- Carve `core-utils/collect.ts`/`reveal.ts`: neutral assembly helpers → `core/`; privacyDB `/v1` builders/parsers stay in `skyflow-js`. + +→ Full task-by-task breakdown in [phase-1-execution-plan.md](phase-1-execution-plan.md). + +### Phase 2 — Physical split + build flowvault from `2.9.0-beta.1` +- Relocate the existing tree into `packages/skyflow-js`; set up npm workspaces for the two SDK packages; keep `core/` at the root. +- **Build `skyflow-flowvault-js` from the `2.9.0-beta.1` flowDB deltas as `@core` extensions:** flowDB element input/response types extending the core bases, flowDB `/v2` data layer (incl. `cvvMap` masking), `skyflow-flowdb-error` extending the core error base, and flowvault's own `tokenize()`/`revealData()` calling the core leaf helpers. Broaden the telemetry language-label check to treat `skyflow-flowvault-js` as `JS`. +- Per-package `index.ts` / `index-node.ts` with correct public names (§6); per-package `package.json` (name / main / types / files). +- Per-package browser + node + iframe webpack configs, each `merge`-ing shared `webpack.common.js`; flowvault's UMD `library` + browser IIFE global = `SkyflowFlowVault` (skyflow-js keeps `Skyflow`). + +### Phase 3 — Build & release +- Parameterize `common-release.yml` with inputs (`PACKAGE_DIR`, `PACKAGE_NAME`, `S3_PREFIX`, iframe URL / `BUILD_IFRAME`); add a thin caller workflow per package. +- Two iframe deploy pipelines (one per package), each to its own S3 prefix + CloudFront. +- Untangle tests (§7). + +### Phase 4 — Samples & docs +- Repoint `samples/using-script-tag/*.html` and `samples/using-typescript/skyflow-elements` at the correct package/CDN URL. +- Move flowDB samples (currently on preview CDN URLs in `using-script-tag`) under the flowvault package. +- Update README(s) per package. + +Critical path is the Phase 1 `core-utils` factoring and validating both iframes against the shared core. The iframe-deploy config cost in Phase 3 is offset by the deleted adapter-injection work in Phase 1. + +--- + +## 6. Backward-compatibility guarantees (`skyflow-js` consumers) + +Because `skyflow-js` is sourced from `main`, its public surface is restored by construction. The guarantee, by tier: + +- **Safe verbatim:** `Skyflow` (default), `CollectContainer/Element`, `Composable*`, `RevealContainer/Element`, `ComposableReveal*`; common types `ContainerOptions`, `CollectElement*`, `CollectOptions`, `AdditionalFields*`, `CardMetadata`, `Input/Label/ErrorTextStyles`, `RedactionType`, `RenderFileResponse`, `ValidationRule(Type)`, `EventName`, `LogLevel`, `Env`, `ElementState`, `ErrorType`, `ErrorMessages`, `UpdateType`, `CardType`, `ElementType`, `ContainerType`, `SkyflowConfig`. +- **Names that must resolve to the PDB shape** (flowDB-bound in `2.9.0-beta.1`, PDB-bound in `skyflow-js`): `CollectResponse`, `CollectRecord`, `RevealResponse`, `RevealRecord`, `RevealElementInput` (regains `skyflowID/table/column`), `RevealOptions`, `RevealElementOptions`, `UpsertOptions` (back to `{table, column}`), `SkyflowError` (neutral `libs/skyflow-error`, not the flowDB class). +- **Restored from `main`:** `InsertResponse`, `UpdateResponse`, `DetokenizeResponse`, `GetResponse`, `GetByIdResponse`, `DeleteResponse`, `UploadFilesResponse` + their request/record types. + +**Canary consumer:** `samples/using-typescript/skyflow-elements/src/index.ts` imports exactly the collision-tier names (`CollectResponse`, `RevealResponse`, `RevealElementInput`, `RevealOptions`, `SkyflowError`) — use it as the compat smoke test for `skyflow-js`. + +`skyflow-flowvault-js` carries the flowDB shapes of these same names — no backward-compat constraint (new package, v1.0.0). + +--- + +## 7. Build & release detail + +- **Webpack:** share `webpack.common.js` (add the `@core` `resolve.alias`; the existing `babel-loader` `exclude: /node_modules/` already compiles `core/` since it is a plain folder — no change needed there). Each package needs only small browser + node + iframe configs differing in `entry`, `output.path`, and UMD `library` name. The iframe config is per-package (each imports the core skeleton via its `index-internal.ts`). +- **Shared-core wiring:** `@core` alias declared once in `tsconfig.base.json` and mirrored to webpack + jest; `import/no-restricted-paths` enforces `core/` ⇏ `packages/`. `core/` is compiled inline into each build — no separate core build, no build ordering. +- **SDK telemetry identity:** inject `SDK_NAME`/`SDK_VERSION` per package via `DefinePlugin` (from each package's own `package.json`); the shared `core/` helper reads these constants instead of importing `package.json`. Keep the language label `JS` for both SDKs by broadening the `sdkName === 'skyflow-js'` check to include `skyflow-flowvault-js`; preserve the existing `metaData` wrapper-override so a wrapper like `skyflow-react@x` still reports `React`. `skyflow-js` output stays byte-identical (`skyflow-js@`, `JS SDK v`). +- **Hardcoded values to watch:** output dirs (`dist/v1`, `dist/sdkNodeBuild`, `dist/v1/elements`), the UMD/IIFE global (`Skyflow` for skyflow-js, `SkyflowFlowVault` for flowvault), and the iframe URL default in each package's `properties.ts`. +- **Release workflow:** `common-release.yml` is already `workflow_call` but hard-assumes one `package.json`, one `dist/v1` S3 prefix, one `npm publish`. Parameterize by package/dir/prefix; per-package caller workflows. **Tags:** skyflow-js keeps bare-semver triggers; flowvault uses `flowvault-`-prefixed tags (configurable via the workflow trigger pattern). **Toolchain:** `setup-node@v4` / Node 18 across CI. +- **npm publish** keys off `package.json` `name`/`version`; each package publishes independently under its own name. + +## 8. Testing + +- `jest.config.json` is minimal: `jsdom`, `collectCoverage: true`, no `roots`, no `coverageThreshold`, only an svg mock. **Per-package coverage config is written from scratch.** +- The 5 dedicated `*.flowdb.test.js` files move mechanically to flowvault: + - `tests/core-utils/collect.flowdb.test.js`, `tests/core-utils/reveal.flowdb.test.js` + - `tests/core/internal/frame-element-init.flowdb.test.js`, `.../composable-frame-element-init.flowdb.test.js` + - `tests/core/internal/skyflow-frame/skyflow-frame-controller.detokenize.flowdb.test.js` +- The PDB test files are **half-migrated** in `2.9.0-beta.1` (flowDB `skip`s + inline `SkyflowFlowDBError` assertions in `skyflow.test.*`, `collect-container`, `reveal-container`, `skyflow-frame-controller`, `frame-element-init.*`, `upload-tokenize`). Since `skyflow-js` is sourced from `main`, take its tests from `main` rather than untangling the beta versions. +- Note many tests are duplicated `.js` + `.ts` — deduplicate opportunistically during the split. + +--- + +## 9. Pre-execution prerequisites (blocker checklist) + +Grouped by when each must be settled. Status reflects decisions taken so far. + +### Resolved / retired (for the record) +- ✅ **Versioned iframe URL delivery** — resolved. Existing `properties.ts` + `DefinePlugin` + release-workflow secrets bake a per-package absolute URL; each package supplies its own origin/site/version (see §3.2). +- ✅ **Workspace transpilation gotcha** — retired by the shared-`core/`-folder decision. Because `core/` is a plain folder (not symlinked under `node_modules`), `babel-loader` compiles it normally; no Option A/B, no build ordering (see §3.1). +- ✅ **Per-package SDK telemetry identity** — resolved. Inject `SDK_NAME`/`SDK_VERSION` per package via `DefinePlugin` from each package's own `package.json`; the shared `core/` helper reads the injected constants instead of `import …/package.json`. flowvault reports `skyflow-flowvault-js@` (no analytics enum to register). Language label stays `JS` for both first-party SDKs — broaden the `sdkName === 'skyflow-js'` check ([helpers/index.ts:476](../src/utils/helpers/index.ts#L476)) to also accept `skyflow-flowvault-js`, leaving the `metaData` wrapper-override (`skyflow-react@x` → `React`) intact. `skyflow-js` telemetry output is unchanged. See §7. +- ✅ **Pure-JS surface ownership** — resolved. `skyflow-flowvault-js` v1.0.0 is **elements-only**; pure-JS `Skyflow.*` methods are deferred to a future release. This is **not a public removal**: on `2.9.0-beta.1` the pure-JS methods are already private (`#insert`/`#detokenize`/… in [skyflow.ts:301-339](../src/skyflow.ts#L301), no public alias; pure-JS types commented out in `index-node.ts`), so flowDB pure-JS was never publicly exposed. `skyflow-js` keeps its pure-JS methods **public** (restored from `main`, where they are public). **`/v2` file-upload/render → `skyflow-js` only**; flowvault has no file support. To keep future flowvault pure-JS additive, keep the pure-JS **transport** (the `PUREJS_REQUEST` dispatch + container→frame-controller flow) variant-neutral in `core/`; only the per-method `/v1` vs `/v2` data layer is package-specific. **Phase-1 verify:** skyflow-js's file-upload endpoint must land on `main`'s behavior, not the beta's `/v2` variant. +- ✅ **3DS / ThreeDS ownership** — resolved. Lives entirely in `skyflow-js`: public on `main` (`ThreeDS` + `ThreeDSBrowserDetails`, [index-node.ts:57,75](../src/index-node.ts)), restored as-is along with the `threeds.ts` module. flowvault has **no** 3DS now (commented out in `2.9.0-beta.1`, never publicly exposed there); a future flowvault 3DS would be additive. 3DS is a package-specific feature, **not** shared `core/`. + +_All Phase-1-blocking decisions are now resolved._ + +### Before Phase 2 (settled) +- ✅ **Toolchain bump for workspaces.** Build/CI moves to **Node 18 LTS** + `setup-node@v4` (from Node 14.17.6). This is a *build/dev* change only — the published `engines` (`node >=12`) stays, so **consumers on older Node are unaffected**. +- ✅ **Git/branch strategy.** Cut a dedicated **integration branch from `main`**; all refactor work targets that branch and merges into it **phase by phase** (not directly to `main`). Use `git mv` for the physical move so `git blame`/`--follow` history is preserved. The integration branch becomes the release source once complete. + +### Before Phase 3 (settled) +- ✅ **Tag namespaces.** `skyflow-js` keeps its existing bare-semver tags (no change to current automation); `skyflow-flowvault-js` uses **`flowvault-`-prefixed** tags (`flowvault-v1.0.0`, `flowvault-v1.0.0-beta.1`) with its own caller workflows filtering `flowvault-*`. The prefix lives only in the workflow trigger pattern — **configurable later** via a one-line workflow edit, no artifact impact. +- ✅ **npm name + publish rights.** Confirmed — publish access ready in dev + prod under the same npm org. +- ✅ **UMD/global name** for flowvault browser bundle = **`SkyflowFlowVault`** (consumer-facing global for script-tag/CDN users; skyflow-js keeps `Skyflow`). Applies to both the UMD `library` name and the browser IIFE (`window.SkyflowFlowVault`) and to flowvault's script-tag samples. +- ✅ **Consumer migration comms — not needed.** The `2.9.0-beta.1` flowDB build was shared privately with a single customer, not published to public npm — so there is no public consumer to migrate and no npm deprecation required. +- ⏳ **Infra request (external — infra team).** Provision flowvault SDK + iframe hosting, mirroring the existing skyflow-js setup so the team can correlate. Hand-off checklist: + 1. **DNS host** — `js-flowvault.skyflow.dev` (prod) + dev/sandbox equivalents matching skyflow-js's per-env hosts. + 2. **TLS** — ACM certificate covering the new host(s). + 3. **CDN** — CloudFront distribution for the new host; **origin = the same S3 bucket** as skyflow-js under a dedicated `flowvault/` sub-prefix; serve `/{version}/elements/*` (iframe) and `/{version}/*` (browser bundle); mirror skyflow-js's cache behaviors. + 4. **S3 write** — grant the CI release role write access to the `flowvault/` prefix. + 5. **Invalidation** — the flowvault CloudFront **distribution ID** + IAM permission for the CI role to invalidate it (skyflow-js's release runs ×10 invalidations). + 6. **CI secrets** (per env, mirroring skyflow-js's `PROD_/SANDBOX_` secrets): + - `FLOWVAULT_{PROD,SANDBOX}_IFRAME_SECURE_ORIGIN` = `https://js-flowvault.skyflow.dev` + - `FLOWVAULT_{PROD,SANDBOX}_IFRAME_SECURE_SITE` = `elements/index.html` + - the flowvault CloudFront distribution ID (for invalidation). + +### Verification gates (settled) +- ✅ **Bundle-size.** One-time check (not a CI gate): capture `skyflow-js`'s current bundle size from `main` as the baseline and compare the post-split `skyflow-js` once. +- ✅ **Coverage.** Floor = the current `main`-branch coverage per package (no backsliding), applied via per-package `coverageThreshold` after the test split. diff --git a/docs/phase-1-execution-plan.md b/docs/phase-1-execution-plan.md new file mode 100644 index 00000000..ffb0192c --- /dev/null +++ b/docs/phase-1-execution-plan.md @@ -0,0 +1,201 @@ +# Phase 1 — Detailed Code Execution Plan (`core/` extraction) + +Companion to [package-split-plan.md](package-split-plan.md). This decomposes **Phase 1** into small, individually-reviewable tasks. Each task is one PR into the integration branch, is **behavior-preserving**, and leaves the build + tests green. + +--- + +## Scope & non-goals + +**In scope (Phase 1):** On an integration branch cut from `main` (privacyDB baseline), extract the variant-neutral code into a shared `core/` folder behind the `@core` alias + boundary lint rule, and leave the existing `skyflow-js` (privacyDB) package working on top of it. + +**Explicit non-goals (deferred to later phases):** +- **No flowDB.** `main` has no flowDB code; flowvault is built from the `2.9.0-beta.1` deltas as `@core` extensions in a **later phase** (it cannot extend a `core/` that doesn't exist yet). +- **No second package / no `packages/` relocation yet.** During Phase 1 the existing tree stays at the repo root as the `skyflow-js` package; `core/` is added alongside it. Creating `packages/skyflow-js` + `packages/skyflow-flowvault-js` and npm workspaces is Phase 2. + +**Invariant after every task:** the public API surface (`index.ts` / `index-node.ts` exports and their shapes), SDK telemetry output, and all existing tests are **unchanged**. Phase 1 is a pure internal refactor. + +### End state of Phase 1 +``` +repo/ + core/ # variant-neutral shared source + @core barrel + src/ # existing skyflow-js (privacyDB), now importing @core + tsconfig.base.json # @core/* → core/* + .eslintrc(.js) # import/no-restricted-paths: core/ ⇏ src/ +``` + +--- + +## Working model + +- **Branch:** one long-lived integration branch off `main` (e.g. `refactor/pkg-split`). Every task below is a PR **into that branch**, reviewed and merged before the next starts. The branch becomes the release source when the whole split is done. +- **One task = one PR.** Ordered; each builds on the previous. Earliest tasks are lowest-risk (pure moves); boundary-inversion tasks come after the leaves are in place. +- **Moves preserve history:** use `git mv` so `git blame`/`--follow` still work. + +### Verification recipe (run at the end of every task) +1. `npm run type-check` — no TS errors. +2. `npm test` — full suite green (no new skips). +3. `npm run build-browser-sdk && npm run build-node-sdk && npm run build-iframe` — all three bundles build. +4. **Public-surface guard:** `npm run build:types` and diff the emitted `types/index-node.d.ts` + `types/index.d.ts` against the pre-Phase-1 snapshot — expect **zero diff** (Tasks that intentionally relocate a type must still produce an identical *exported* shape). +5. **Consumer canary:** `samples/using-typescript/skyflow-elements` still type-checks against the built package. +6. **Boundary lint:** `npx eslint` passes, including `import/no-restricted-paths` (warn until Task 1.11, then error). + +### Definition of done (per task) +Verification recipe green · reviewer approves · no change to public surface or telemetry (except where a task explicitly restructures internals with an identical external shape). + +--- + +## Task list + +### Task 1.0 — Branch, toolchain, empty boundary scaffolding +**Goal:** stand up the `@core` boundary with nothing moved yet. +- Cut `refactor/pkg-split` from `main`. Capture the baselines: `build:types` snapshot (for the surface guard), current bundle size (webpack-bundle-analyzer), current coverage numbers. +- CI: `actions/setup-node@v4`, Node 18 (build/dev only; leave published `engines` untouched). +- Create empty `core/` + `core/index.ts` (empty barrel). +- Add the `@core` alias in **one source of truth** (`tsconfig.base.json` `paths: { "@core/*": ["core/*"] }`) mirrored to `webpack.common.js` `resolve.alias` and `jest.config.json` `moduleNameMapper`. +- Add `eslint-plugin-import` `import/no-restricted-paths` zone (`core/` ⇏ `src/`), severity **warn** for now. + +**Reviewability:** config-only diff; no logic touched. +**Verify:** recipe green; surface snapshot captured. + +--- + +### Task 1.1 — Move zero-import neutral leaves +**Goal:** move the leaves that import nothing (safest first). +- `git mv` → `core/`: `libs/uuid.ts`, `libs/regex.ts`, `libs/deep-clone.ts`, `utils/jwt-utils/`, `libs/jss-styles.ts`, `event-emitter/`, `libs/bus.ts`. +- Repoint importers to `@core/...`; add these to the `core/index.ts` barrel. + +**Reviewability:** pure move + import rewrite; small. +**Verify:** type-check + tests green. + +--- + +### Task 1.2 — Move logging, error-codes, DOM/iframe & metrics primitives +**Goal:** move the neutral, downward-only utility tier. +- `git mv` → `core/`: `utils/logs.ts`, `utils/constants.ts` (`SKYFLOW_ERROR_CODE`), `core/constants.ts`, `properties.ts`, `iframe-libs/iframer.ts`, `utils/bus-events/`, `metrics/`. +- `utils/logs-helper/` moves too, **but** it depends on `helpers.getSDKLanguageAndVersion` — pull that one neutral helper across with it (or temporarily import from `src`) and finish the helpers move in Task 1.3. + +**Reviewability:** move + import rewrite; watch the `logs-helper → helpers` edge. +**Verify:** recipe green. + +--- + +### Task 1.3 — SDK telemetry identity injection + neutral helpers +**Goal:** make SDK name/version injectable per package (prerequisite for two packages) with **identical** output for skyflow-js. +- Replace `import SDKDetails from '../../../package.json'` ([helpers/index.ts:17](../src/utils/helpers/index.ts#L17)) with build-time `SDK_NAME` / `SDK_VERSION` injected via `DefinePlugin` (mirroring the existing `IFRAME_SECURE_*` injection); the shared helper reads the injected constants. +- Restructure the language-label ([helpers/index.ts:476](../src/utils/helpers/index.ts#L476)) so it's injection-ready (skyflow-js → `JS`), preserving output. (The `=== 'skyflow-js'` broadening for flowvault happens when flowvault is built — not now.) +- Move the remaining neutral helper functions into `core/helpers`. + +**Reviewability:** localized to helpers + webpack define; call out the telemetry-preserving intent. +**Verify:** recipe green **plus** an explicit telemetry check — `sdk_name_version` still equals `skyflow-js@` and the label `JS SDK v` (snapshot before/after). + +--- + +### Task 1.4 — Neutral common types → `core/types` + base interfaces +**Goal:** carve the neutral type surface out of `utils/common` and define the base interfaces packages will extend. +- Move to `core/types`: enums (`EventName`, `LogLevel`, `Env`, `RedactionType`, `UpdateType`, `ValidationRuleType`, `ErrorType`, `MessageType`, `RequestMethod`), element/style types (`Style`, `ContainerOptions`, `Input/Label/ErrorTextStyles`, `CollectElement*`, `ICollectOptions`, `ElementState`, `AdditionalFields*`, `CardMetadata`), and the neutral record/response families. +- Define **base interfaces**: base element-input, base record/response envelope, base upsert options — the extension points for privacyDB (now) and flowDB (later). +- Keep privacyDB-specific types in `src`, extending the base. +- Re-export everything from `index-node.ts`/`index.ts` under the **same public names**. + +**Reviewability:** larger but mechanical; the surface guard is the safety net. +**Verify:** recipe green; **surface diff must be zero** (same exported names + shapes); using-typescript canary compiles. + +--- + +### Task 1.5 — Invert back-edge: validators +**Goal:** stop shared validators importing "up" into container/`skyflow.ts` types. +- Today `validators/index.ts` imports `IRevealElementInput` (`reveal-container`) and `ISkyflow` (`skyflow.ts`). +- Split: pure validators (card/Luhn/regex/format) → `core/validators`. For request/shape validators that reference input types, **relocate those input interfaces into `core/types`** (Task 1.4's base types) so the dependency points down, not up. + +**Reviewability:** focused on one file + the relocated interfaces. +**Verify:** recipe green; boundary lint clean for the moved validators. + +--- + +### Task 1.6 — Invert back-edges: helpers & element-options +**Goal:** clear the remaining shared→container type edges. +- `helpers/index.ts` imports `IRevealElementOptions` (`reveal-container`) + `ContainerType`/`ISkyflow` (`skyflow.ts`). Move `ContainerType` (neutral) to `core`; relocate the referenced input interfaces to `core/types`. +- `libs/element-options.ts` imports concrete `CollectElement`/`ComposableElement` classes — restructure so it depends on `core` interfaces, not concrete element classes (invert via a small interface). + +**Reviewability:** two files; each edge removal is independently checkable. +**Verify:** recipe green. + +--- + +### Task 1.7 — Split `internal-types` (neutral → core) +**Goal:** remove the barrel/`skyflow.ts`/`skyflow-container` upward imports from `internal-types`. +- `internal-types/index.ts` imports the `index-node` barrel, `skyflow.ts`, and `external/skyflow-container` (upward edges). +- Move the neutral internal types (`ElementInfo`, `InternalState`, `Metadata`, `ClientMetadata`, `SkyflowElementProps`, etc.) → `core/types`; break the barrel import by referencing concrete types directly. + +**Reviewability:** type-only moves; surface guard covers regressions. +**Verify:** recipe green. + +--- + +### Task 1.8 — `skyflow-error` base + styles into core +**Goal:** move the neutral error base and style helpers. +- Move `libs/skyflow-error.ts` (the neutral base) → `core/errors`; keep the public `SkyflowError` exported from the package via a re-export from `@core`. +- Move `libs/styles.ts` + neutral parts of `element-options` → `core`. + +**Reviewability:** small; verify the public `SkyflowError` identity is unchanged. +**Verify:** recipe green; `SkyflowError` still exported with identical shape. + +--- + +### Task 1.9 — Extract frame leaf-helpers into core +**Goal:** move the variant-neutral element/frame helpers that both packages will call (the §3.4 leaf helpers). +- Extract into `core` as standalone helpers: the element **validation pass** (`validateElements(options, ctx) → errorMessage`), `getUnformattedValue`, frame/element lookup, checkbox concatenation, duplicate-element detection (`checkForElementMatchRule` / `checkForValueMatch`, currently in `core-utils/collect.ts:471-481`). +- Rewire the privacyDB frame controller + `iframe-form` to call the `@core` helpers. + +**Reviewability:** behavior-preserving extraction; element tests are the guard. +**Verify:** collect/reveal element tests green. + +--- + +### Task 1.10 — Carve `core-utils/collect.ts` & `reveal.ts` (privacyDB) against core +**Goal:** separate neutral request-assembly plumbing from privacyDB transport. +- Move neutral helpers → `core`: `constructElementsInsertReq` (`collect.ts:188`), `formatRecordsForIframe` (`reveal.ts:340`), and any other variant-neutral formatter. +- Leave privacyDB `/v1` builders/parsers in `src` (`constructInsertRecordRequest/Response`, `constructUpdate*`, `insertDataInCollect`, `fetchRecordsByTokenId`, `formatRecordsForClient`, GET/render helpers), now consuming the `@core` helpers. + +**Reviewability:** the two hardest files, but privacyDB-only (no flowDB interleaving on `main`), so it's a clean neutral-vs-transport cut. +**Verify:** recipe green; collect/reveal/detokenize tests green. + +--- + +### Task 1.11 — Pure-JS transport neutral + barrel finalize + boundary hardening +**Goal:** finish the boundary and lock it. +- Ensure the pure-JS **transport** (the `PUREJS_REQUEST` dispatch + container→frame-controller message flow skeleton) is neutral in `core` so future flowvault pure-JS is additive; the `/v1` data layer stays in `src`. +- Finalize `core/index.ts` — the barrel is the surface flowvault will consume in a later phase. +- Flip `import/no-restricted-paths` to **error**. +- Run the full build; **capture the post-Phase-1 bundle-size** (compare to the Task 1.0 baseline, expect ≤ +2%) and confirm coverage ≥ the `main` floor. + +**Reviewability:** mostly wiring + config; final green-field check. +**Verify:** full recipe green; **public surface + telemetry diff = zero**; bundle-size within tolerance; coverage ≥ floor. + +--- + +## Dependency ordering + +``` +1.0 (scaffold) + └─ 1.1 (zero-import leaves) + └─ 1.2 (logs/constants/dom/metrics) + └─ 1.3 (telemetry inject + helpers) + └─ 1.4 (neutral types + base interfaces) ← unblocks the back-edge inversions + ├─ 1.5 (validators) + ├─ 1.6 (helpers/element-options) + └─ 1.7 (internal-types) + └─ 1.8 (error base + styles) + └─ 1.9 (frame leaf-helpers) + └─ 1.10 (collect/reveal carve) + └─ 1.11 (pure-JS transport + finalize) +``` + +Tasks 1.5–1.7 can be parallel PRs once 1.4 lands. Everything else is linear. + +## Risks & watch-items +- **Surface drift:** the emitted-`.d.ts` diff (recipe step 4) is the primary guard — treat any non-empty diff as a defect unless the task intends an identical re-export. +- **Telemetry regression (Task 1.3):** verify `sdk_name_version` explicitly; it's easy to change silently by moving the `package.json` read. +- **`logs-helper → helpers` edge (Task 1.2):** the one ordering hazard in the leaf moves — carry `getSDKLanguageAndVersion` across with it. +- **Circular imports:** relocating interfaces into `core/types` (1.4–1.7) can create cycles if a `core` type pulls a `src` type — the boundary lint rule (error in 1.11) catches these; keep base interfaces dependency-free. +- **Test duplication:** `main` has duplicated `.js`/`.ts` tests — update both (or dedupe) as files move so coverage doesn't drop. diff --git a/docs/phase-2-execution-plan.md b/docs/phase-2-execution-plan.md new file mode 100644 index 00000000..ba114f20 --- /dev/null +++ b/docs/phase-2-execution-plan.md @@ -0,0 +1,180 @@ +# Phase 2 — Detailed Code Execution Plan (packages + build flowvault) + +Companion to [package-split-plan.md](package-split-plan.md); follows [phase-1-execution-plan.md](phase-1-execution-plan.md). Same model: each task is one PR into the integration branch, individually reviewable, leaving the build + tests green. + +--- + +## Entry state (end of Phase 1) +``` +repo/ + core/ # variant-neutral shared source + @core barrel + src/ # skyflow-js (privacyDB), importing @core + tests/ # skyflow-js tests + tsconfig.base.json # @core/* → core/* + .eslintrc(.js) # import/no-restricted-paths: core/ ⇏ src/ +``` + +## Scope + +Phase 2 does two things: +- **(A) Relocate** the existing tree into `packages/skyflow-js` and stand up npm **workspaces** (skyflow-js behavior-preserving). +- **(B) Build `skyflow-flowvault-js`** (v1.0.0, **elements-only**) from the `2.9.0-beta.1` flowDB deltas, reshaped to **extend `@core`** (base types, error base, leaf helpers) rather than redefine them. + +### Non-goals (deferred) +- **No release-workflow changes** — parameterizing `common-release.yml`, S3/CloudFront, npm publish is **Phase 3**. Phase 2 ends when both packages build all artifacts locally. +- **flowvault stays elements-only** — no pure-JS `Skyflow.*`, no 3DS, no file upload (per §9 decisions). Keep the pure-JS transport neutral in `core/` but do not expose flowvault pure-JS. + +### End state of Phase 2 +``` +repo/ + core/ + packages/ + skyflow-js/ # privacyDB (relocated) — name "skyflow-js" + src/ tests/ package.json tsconfig.json webpack.*.js + skyflow-flowvault-js/ # flowDB (new, v1.0.0) — extends @core + src/ tests/ package.json tsconfig.json webpack.*.js + package.json # private workspace root ("workspaces": ["packages/*"]) + tsconfig.base.json + .eslintrc(.js) # core/ ⇏ packages/ +``` + +**Invariants:** `skyflow-js`'s public surface + telemetry + tests stay **unchanged** throughout. `skyflow-flowvault-js` is anchored to the **ported `*.flowdb.test.js` suites** (its behavioral contract from `2.9.0-beta.1`). + +--- + +## Working model +Same integration branch as Phase 1. One task = one PR, reviewed and merged before the next. `git mv` for all relocations (preserve history). flowvault code is **extracted from the `2.9.0-beta.1` tag** and reshaped onto `@core` — not copied verbatim. + +### Verification recipe (end of every task) +1. `npm install` — workspaces link cleanly. +2. `npm run type-check` (per affected package) — no TS errors. +3. `npm test` (per affected package) — green, no new skips. +4. Build the affected package's artifacts (`build-browser-sdk` / `build-node-sdk` / `build-iframe`). +5. **skyflow-js surface guard:** emitted `types/*.d.ts` diff against the Phase-1-end snapshot = **zero** (relocation must not change the published surface). +6. **flowvault behavior anchor:** the ported `*.flowdb.test.js` suites pass. +7. **Boundary lint:** `import/no-restricted-paths` (`core/` ⇏ `packages/`) passes; and `packages/skyflow-flowvault-js` does **not** import `packages/skyflow-js` (add a zone for that too). +8. **Telemetry snapshot:** skyflow-js → `skyflow-js@`; flowvault → `skyflow-flowvault-js@`, label `JS`. + +--- + +## Group A — Relocate skyflow-js + workspaces + +### Task 2.0 — Workspaces + relocate `skyflow-js` → `packages/skyflow-js` +**Goal:** move the existing package under `packages/` and make the repo a workspace root, with **zero** behavior change. +- Convert the **root `package.json`** into a private workspace root: `"private": true`, `"workspaces": ["packages/*"]`; move the SDK manifest fields (`name` `skyflow-js`, `version`, `main`, `types`, `files`, `scripts`, `dependencies`) into **`packages/skyflow-js/package.json`**. +- `git mv src → packages/skyflow-js/src`, `git mv tests → packages/skyflow-js/tests`, and the SDK's `webpack.*.js` / `jest.config.json` / `tsconfig.json` into the package. +- Fix `@core` resolution for the new depth: keep `tsconfig.base.json` (`baseUrl` at repo root, `@core/* → core/*`); each package `tsconfig.json` `extends` it; update `webpack resolve.alias` → `../../core`; update `jest moduleNameMapper`. +- Root convenience scripts delegate to workspaces (`npm run build -w skyflow-js`, etc.). + +**Reviewability:** large but purely mechanical (moves + path fixes); the surface guard is the safety net. +**Verify:** recipe 1–5; skyflow-js builds all three artifacts; **surface diff = zero**; tests green. + +--- + +## Group B — Build `skyflow-flowvault-js` from `2.9.0-beta.1` as `@core` extensions + +> Each task below **extracts the flowDB slice from the `2.9.0-beta.1` tag** and reshapes it to consume `@core`. flowvault has **no existing consumers**, so "correct" = matches the beta's flowDB behavior, proven by the ported `*.flowdb.test.js` suites. + +### Task 2.1 — Scaffold `packages/skyflow-flowvault-js` +**Goal:** an empty-but-buildable package wired into the workspace. +- `package.json`: `name` `skyflow-flowvault-js`, `version` `1.0.0`, `main`/`types`/`files` mirroring skyflow-js's shape, `dependencies` (same runtime deps). +- `tsconfig.json` extends `tsconfig.base.json`; empty `src/index.ts` (sets `window.SkyflowFlowVault`) + `src/index-node.ts` (empty barrel) + `src/index-internal.ts` (iframe entry stub). +- Register in the boundary lint (flowvault ⇏ skyflow-js). + +**Verify:** `npm install` links it; `type-check` green (no-op package). + +### Task 2.2 — flowDB types extending `@core` bases +**Goal:** the flowDB type surface as **extensions** of the core base interfaces (not redefinitions). +- Port from the beta: the flowDB internal types (`FlowDBInsert*`, `FlowDBUpdate*`, `FlowDBDetokenize*`, `FlowDBRecordResponse`, `FlowDBError`/`FlowDBFullError`, `CollectRecord/Response`, `RevealRecord/Response`, `FlowDBTokenGroupRedaction`) and the public input types (`IFlowDBUpsertOptions`, `IFlowDBRevealElementInput`, `IRevealElementOptions`, `IRevealOptions`, `TokenGroupRedaction`). +- Reshape so element-input/response/upsert types **`extends`** the `@core` base interfaces from Phase 1 Task 1.4. + +**Verify:** `type-check` green; the flowDB response/input shapes match the beta's public contract. + +### Task 2.3 — `skyflow-flowdb-error` extending the `@core` error base +**Goal:** flowvault's error class on top of the neutral base. +- Port `libs/skyflow-flowdb-error.ts` (`SkyflowFlowDBError`, `normalizeFlowDBError`); make `SkyflowFlowDBError` **extend** the `@core` `SkyflowError` base. It remains flowvault's public `SkyflowError` export. + +**Verify:** `type-check`; a small unit test for `normalizeFlowDBError` (snake→camel) passes. + +### Task 2.4 — flowDB **collect** data layer (`/v2`) +**Goal:** flowvault's collect transport, using `@core` assembly helpers. +- Port from beta `core-utils/collect.ts` (flowDB slice): `getFlowDBUpsertForTable`, `constructFlowDBInsertRequest`, `constructFlowDBInsertResponse`, `constructFlowDBInsertError`, `constructFlowDBUpdateRequest`, `flowDBInsertVariant`/`flowDBUpdateVariant`, `executeInsert`, `insertDataInCollectFlowDB`, `updateDataInCollectFlowDB`, and **`replaceCVVTokensInResponse` (cvvMap masking — flowvault-only)**. +- Consume `@core` neutral helpers (`constructElementsInsertReq`, validators) rather than redefining them. + +**Verify:** port `tests/core-utils/collect.flowdb.test.js` → green. + +### Task 2.5 — flowDB **reveal/detokenize** data layer (`/v2`) +**Goal:** flowvault's reveal transport. +- Port from beta `core-utils/reveal.ts` (flowDB slice): `constructFlowDBDetokenizeRequest/Response/Error`, `flowDBDetokenizeVariant`, `executeDetokenize`, `fetchRecordsByTokenIdFlowDB`, `fetchRecordsByTokenIdComposableFlowDB`, `normalizeFlowDBMetadata`, `formatRecordsForClientFlowDB`, `formatRecordsForClientComposableFlowDB`. +- Consume `@core` neutral formatters where they exist. + +**Verify:** port `tests/core-utils/reveal.flowdb.test.js` → green. + +### Task 2.6 — flowvault **collect** path (containers, elements, frame controller) +**Goal:** wire the collect element flow end-to-end for flowDB. +- Bring in the collect container + element + `frame-element-init` + the frame-controller `tokenize()`, adapting skyflow-js's core-based scaffolding: swap in `SkyflowFlowDBError`, the flowDB input/response types, and calls to the flowvault collect data layer (Task 2.4). The `tokenize()` calls `@core` leaf helpers (validate/assemble) then flowvault's request-build + response-parse (§3.4). + +**Verify:** port `frame-element-init.flowdb.test.js` + the frame-controller collect/tokenize flowDB tests → green. + +### Task 2.7 — flowvault **reveal** path (containers, elements, composable) +**Goal:** wire the reveal element flow end-to-end for flowDB. +- Bring in reveal container + element + composable-reveal + `composable-frame-element-init` + the frame `revealData()`, wired to `@core` leaf helpers + the flowvault reveal data layer (Task 2.5) + `SkyflowFlowDBError`. Use `IFlowDBRevealElementInput` (token-only) as the public reveal input. + +**Verify:** port `composable-frame-element-init.flowdb.test.js` + `skyflow-frame-controller.detokenize.flowdb.test.js` → green. + +### Task 2.8 — flowvault `Skyflow` class + iframe entry + public barrels +**Goal:** the package's public shell. +- flowvault `Skyflow` class + `container()` factory (elements-only: COLLECT / COMPOSABLE / REVEAL / COMPOSE_REVEAL; **no** pure-JS methods). +- `index-internal.ts` composes the `@core` iframe skeleton + flowvault's frame controllers (its own iframe build). +- `index.ts` sets `window.SkyflowFlowVault`; `index-node.ts` exports the flowDB public surface with the agreed names (`UpsertOptions`=`IFlowDBUpsertOptions`, `RevealElementInput`=`IFlowDBRevealElementInput`, `CollectResponse/Record`, `RevealResponse/Record`, `RevealOptions`, `SkyflowError`=`SkyflowFlowDBError`, plus the shared enums/classes re-exported from `@core`). + +**Verify:** `type-check`; all agreed flowDB public names are exported with the right shapes. + +### Task 2.9 — flowvault webpack configs + telemetry + iframe URL +**Goal:** produce flowvault's three artifacts with correct identity. +- Per-package `webpack.skyflow-browser.js` / `webpack.skyflow-node.js` / `webpack.iframe.js`, each `merge`-ing the shared `webpack.common.js`; **UMD `library` + IIFE global = `SkyflowFlowVault`**; own `output.path`. +- Telemetry: `DefinePlugin` injects `SDK_NAME=skyflow-flowvault-js` + `SDK_VERSION` from flowvault's `package.json`; **broaden the `@core` language-label check to treat `skyflow-flowvault-js` as `JS`** (leaving the `metaData` wrapper-override → `React` intact). +- flowvault `properties.ts` default iframe URL (`process.env`-based, per §3.2). + +**Verify:** all three flowvault artifacts build; telemetry snapshot = `skyflow-flowvault-js@`, label `JS`; skyflow-js telemetry still `skyflow-js@` (label unchanged). + +### Task 2.10 — flowvault tests, coverage, samples +**Goal:** lock behavior and give consumers examples. +- Land the ported `*.flowdb.test.js` suites under `packages/skyflow-flowvault-js/tests`; add `coverageThreshold` = floor (Phase-1 baseline convention). +- Port the beta's flowDB samples into flowvault samples (script-tag using the `SkyflowFlowVault` global; the `using-typescript` flow against `skyflow-flowvault-js`). +- flowvault `README` stub. + +**Verify:** flowvault coverage ≥ floor; sample type-checks/loads. + +### Task 2.11 — Workspace finalize +**Goal:** both packages build green together; lock the boundary. +- Root scripts: `build`/`test`/`type-check` across `--workspaces`. +- Flip any remaining boundary lint to **error** (core ⇏ packages; flowvault ⇏ skyflow-js). +- Full build of **all six artifacts** (browser/node/iframe × 2 packages); capture flowvault bundle size; confirm skyflow-js size still within the Phase-1 baseline tolerance. + +**Verify:** full recipe green for both packages; skyflow-js surface diff = zero; both telemetry snapshots correct. + +--- + +## Dependency ordering +``` +2.0 (relocate + workspaces) + └─ 2.1 (scaffold flowvault) + └─ 2.2 (flowDB types) + └─ 2.3 (flowDB error base) + ├─ 2.4 (collect data /v2) ── 2.6 (collect path) + └─ 2.5 (reveal data /v2) ─── 2.7 (reveal path) + └─ 2.8 (Skyflow class + entries + barrels) + └─ 2.9 (webpack + telemetry + iframe URL) + └─ 2.10 (tests + coverage + samples) + └─ 2.11 (workspace finalize) +``` +2.4/2.5 parallel after 2.3; 2.6/2.7 parallel after their data layers. + +## Risks & watch-items +- **skyflow-js relocation (2.0):** the risk is `@core`/build-path breakage, not logic. The zero-surface-diff + green build is the gate; keep it one atomic PR so there's no half-moved broken state. +- **Reshape drift (2.2):** the beta redefined types; here they must **extend** `@core` bases while keeping the beta's *public* flowDB shapes. Diff the flowvault `.d.ts` against the beta's documented flowDB contract. +- **cvvMap stays in flowvault (2.4):** it is flowDB-only — never let it leak back into `core/`. +- **Accepted duplication:** flowvault's containers/elements/`Skyflow` class largely mirror skyflow-js's (they differ only by error class, input/response types, and data-layer calls). This is the deliberate loose-coupling trade-off (§3.4) — don't "fix" it by hoisting a template-method controller base into core. +- **Elements-only guard:** ensure no pure-JS `Skyflow.*`, 3DS, or file-upload path is exposed on flowvault (they exist in the beta as internal `#`/commented code — leave them out). +- **Two iframe entries in sync (2.8):** `index-internal.ts` in both packages must stay structurally parallel over the shared `@core` skeleton; keep the entries thin. diff --git a/docs/phase-3-execution-plan.md b/docs/phase-3-execution-plan.md new file mode 100644 index 00000000..edc077e8 --- /dev/null +++ b/docs/phase-3-execution-plan.md @@ -0,0 +1,169 @@ +# Phase 3 — Detailed Code Execution Plan (build & release) + +Companion to [package-split-plan.md](package-split-plan.md); follows [phase-2-execution-plan.md](phase-2-execution-plan.md). Same model: each task is one PR into the integration branch, individually reviewable. Because these are CI/CD changes, "reviewable" also means **validated in sandbox, never against prod, and skyflow-js's release path is never broken**. + +--- + +## Entry state (end of Phase 2) +``` +repo/ + core/ + packages/skyflow-js/ # privacyDB, builds all 3 artifacts locally + packages/skyflow-flowvault-js/ # flowDB v1.0.0, builds all 3 artifacts locally + package.json # private workspace root +``` +Both packages build locally; **no release automation is package-aware yet.** + +## Scope +Make the release + CI pipeline serve **two independently-versioned packages** from one repo: +- Parameterize `common-release.yml` by package (dir, name, dist path, S3 prefix, publish target, version/tag parsing). +- Per-package **caller workflows** with per-package **tag namespaces** and **secrets** (two iframe deploy pipelines). +- Workspace-aware CI (`pr.yml` / `main.yml`) + `bump_version.sh`. +- Toolchain → Node 18 in the release workflow (CI was bumped in Phase 1). + +### Non-goals +- **No prod cutover in Phase 3.** All validation is in **sandbox**. The integration branch's workflows go live only when the branch merges to `main` (a deliberate, separate cutover step). +- No changes to the SDK code (that's Phases 1–2). + +### End state +- `common-release.yml` is package-parameterized; **skyflow-js releases exactly as before** (defaults preserve current behavior). +- `skyflow-flowvault-js` releases from `flowvault-`-prefixed tags to its own S3 prefix / CloudFront / npm name. +- CI builds + tests both packages. + +--- + +## Working model & golden rules +Same integration branch; one PR per task. For release-workflow tasks: +1. **skyflow-js first, unchanged.** Parameterize with defaults that reproduce today's skyflow-js behavior; prove it in **sandbox** before adding flowvault. +2. **Sandbox before prod, always.** Validate every path against SANDBOX secrets/buckets/distributions. Prod secrets are untouched until the merge-time cutover. +3. **Additive for flowvault.** flowvault gets *new* caller workflows + *new* `FLOWVAULT_*` secrets; nothing skyflow-js depends on is modified in place beyond the shared reusable workflow. + +### Verification recipe (per task) +1. **`actionlint`** (or YAML validation) passes on changed workflows. +2. **CI green** on a PR to the integration branch (for `pr.yml`/`main.yml` tasks). +3. **Sandbox dry-run** (for release tasks): push a throwaway tag in the sandbox env and confirm — correct package version bumped, artifacts at the right **S3 prefix**, iframe reachable at the right **host**, npm published under the right **name/dist-tag**, CloudFront invalidation hit the right **distribution**. +4. **skyflow-js regression guard:** a sandbox skyflow-js release produces the **same** outputs (version scheme, S3 path `v{version}/`, npm name/tag) as before the change. + +### Definition of done (per task) +Recipe green · reviewer approves · prod secrets untouched · skyflow-js sandbox release still correct. + +--- + +## Task list + +### Task 3.0 — Workspace-aware CI (`pr.yml` + `main.yml`), Node 18 +**Goal:** PR/main CI builds and tests **both** packages. +- `actions/setup-node@v4`, Node 18. +- Run `type-check` + `test` + all three builds **per package** (a `matrix: package: [skyflow-js, skyflow-flowvault-js]`, or `npm run -``` - - -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`
    • `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/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/packages/skyflow-flowvault-js/README.md b/packages/skyflow-flowvault-js/README.md new file mode 100644 index 00000000..a56ed207 --- /dev/null +++ b/packages/skyflow-flowvault-js/README.md @@ -0,0 +1,2950 @@ +# skyflow-flowvault-js +Skyflow's Flow vault JavaScript SDK lets 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) +[![npm](https://img.shields.io/npm/v/skyflow-flowvault-js.svg)](https://www.npmjs.com/package/skyflow-flowvault-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 +- [**Installation**](#installation) + - [Configuration (script tag vs. npm)](#configuration-script-tag-vs-npm) +- [**Initializing Skyflow.js**](#initializing-skyflowjs) +- [**Quick Start**](#quick-start) +- [**Securely collecting data client-side**](#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) + - [Validations](#validations) + - [Event Listener on Collect Elements](#event-listener-on-collect-elements) + - [UI Error for Collect Elements](#ui-error-for-collect-elements) + - [Override default error messages](#override-default-error-messages) + - [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) +- [**Securely collecting data client-side using Composable Elements**](#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 Elements](#set-an-event-listener-on-composable-elements) + - [Update Composable Elements](#update-composable-elements) + - [Event Listener on Composable Container](#set-an-event-listener-on-a-composable-container) +- [**Securely revealing data client-side**](#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) + - [Override default error messages](#override-default-error-messages-1) + - [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) +- [**Securely revealing data client-side using Composable Elements**](#securely-revealing-data-client-side-using-composable-elements) + - [Using Composable Reveal Elements to reveal data](#using-composable-reveal-elements-to-reveal-data) + - [Update Reveal Composable Elements](#update-reveal-composable-elements) +- [**Reporting a Vulnerability**](#reporting-a-vulnerability) +--- + +# Installation + +## Configuration (script tag vs. npm) + +Using script tag + +```html + +``` + + +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 File Element to upload a file**](#using-skyflow-file-element-to-upload-a-file) + +## 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`. +* 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 +} +``` +`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('#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({ + 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 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(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', () => { + 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(); + +``` + +## 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 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(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. +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', + } + } +}); +``` + +--- + +# 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/samples/README.md b/packages/skyflow-flowvault-js/samples/README.md new file mode 100644 index 00000000..0e0bc67a --- /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 00000000..863413b4 --- /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 00000000..da35d37b --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/composable-elements/package.json @@ -0,0 +1,17 @@ +{ + "name": "composableelements", + "version": "1.0.0", + "description": "A Sample on how to add Composable Elements ", + "main": "index.js", + "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 00000000..d4020c6c --- /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 00000000..9d725d39 --- /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 00000000..e3e002e2 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/custom-validations/package.json @@ -0,0 +1,18 @@ +{ + "name": "customvalidations", + "version": "1.0.0", + "description": "", + "main": "index.js", + "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 00000000..be714449 --- /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 00000000..1f4cbfa5 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-input-formatting/package.json @@ -0,0 +1,18 @@ +{ + "name": "skyflowelements", + "version": "1.0.0", + "description": "", + "main": "index.js", + "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 00000000..bf0a3e3c --- /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 00000000..08b7543f --- /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 00000000..becc001d --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements-update-records/package.json @@ -0,0 +1,18 @@ +{ + "name": "skyflow-elements-update-records", + "version": "1.0.0", + "description": "", + "main": "index.js", + "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 00000000..e384775c --- /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: '', + type: Skyflow.ElementType.CARD_NUMBER, + }); + + const cvvElement = collectContainer.create({ + tableName: 'table1', + column: 'cvv', + ...collectStylesOptions, + label: 'Cvv', + placeholder: 'cvv', + type: Skyflow.ElementType.CVV, + skyflowId: '', + }); + + const expiryDateElement = collectContainer.create({ + tableName: 'table1', + column: 'expiry_date', + ...collectStylesOptions, + label: 'Expiry Date', + placeholder: 'MM/YYYY', + type: Skyflow.ElementType.EXPIRATION_DATE, + skyflowId: '', + }); + + 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: '', + 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 00000000..cd4fd903 --- /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": "", + "main": "index.js", + "scripts": { + "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 00000000..76df443a --- /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 00000000..1f4cbfa5 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-npm/skyflow-elements/package.json @@ -0,0 +1,18 @@ +{ + "name": "skyflowelements", + "version": "1.0.0", + "description": "", + "main": "index.js", + "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/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 00000000..7ce8e9f1 --- /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 00000000..1217e85a --- /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/card-brand-choice.html b/packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html new file mode 100644 index 00000000..94091520 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html @@ -0,0 +1,244 @@ + + + + + + + + Skyflow Elements + + + + + +

    Collect Elements

    + +
    +
    + +
    + +
    + +
    +
    
    +        
    +
    + + + + + + \ No newline at end of file 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 00000000..8ace785a --- /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 00000000..7a627f21 --- /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 00000000..3f54a746 --- /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 00000000..e27485d8 --- /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 00000000..7d1ee4a7 --- /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 00000000..aa2f8f39 --- /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 00000000..f6e60952 --- /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 00000000..e002daa2 --- /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 00000000..2fc9c6e2 --- /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 00000000..efd76811 --- /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 00000000..43c82379 --- /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 00000000..286ba59c --- /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 00000000..72a1f0c4 --- /dev/null +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/upsert-support.html @@ -0,0 +1,227 @@ + + + + + + + + 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 00000000..797e1522 --- /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 00000000..a22f3f2d --- /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 00000000..307a1ab4 --- /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 00000000..7997fab1 --- /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 00000000..64a69d3f --- /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 00000000..b743ed3b --- /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 00000000..594c54f4 --- /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 00000000..47b93b9b --- /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 00000000..1779b621 --- /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 00000000..168fa8ed --- /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 00000000..89ad4064 --- /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 00000000..ab5554ba --- /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 00000000..b24dbeee --- /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 00000000..db2c4177 --- /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 00000000..8ceb5fa5 --- /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: '', + 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: '', + }; + 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: '', + }; + 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: '', + }, + { + 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 00000000..642ded5c --- /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 00000000..863cd233 --- /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 00000000..89ad4064 --- /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 00000000..8cf323ea --- /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-js/README.md b/packages/skyflow-js/README.md new file mode 100644 index 00000000..9ccaca50 --- /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/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 c37ca487..39dac5fc 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 00000000..cbb8330a --- /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 00000000..50f050bf --- /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 00000000..62dcbcd4 --- /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 00000000..394c9016 --- /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 00000000..f749af38 --- /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 100% 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 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 100% rename from samples/using-npm/skyflow-elements-update/package.json rename to packages/skyflow-js/samples/using-npm/skyflow-elements-update/package.json 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 00000000..b7f057ea --- /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 00000000..513acbe0 --- /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 100% rename from samples/using-script-tag/composable-file-upload.html rename to packages/skyflow-js/samples/using-script-tag/composable-file-upload.html 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 100% 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 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 100% rename from samples/using-script-tag/skyflow-file-upload.html rename to packages/skyflow-js/samples/using-script-tag/skyflow-file-upload.html 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 00000000..cbb8330a --- /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 00000000..9dae5bf5 --- /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 00000000..ec64d0c6 --- /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 00000000..10d2117b --- /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 00000000..0f7c8cff --- /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 00000000..cdbb516e --- /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 00000000..c34cf431 --- /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 c4ce7d5c..69aa7b4c 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 00000000..9c287b53 --- /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 00000000..78be0508 --- /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 100% 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 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 00000000..c34cf431 --- /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 00000000..9bd5c28a --- /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 00000000..1bc946bf --- /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 From edfa90a596983876e675e557cf2fd6c31497dbf9 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 18 Aug 2026 12:50:38 +0530 Subject: [PATCH 076/103] SK-3041:Fix minor issues with types & tests. --- .eslintignore | 1 + .../collect/composable-collect-element.ts | 10 +- jest.config.js | 20 +++ package.json | 3 +- .../skyflow-flowvault-js/jest.config.json | 8 ++ .../src/external/collect/collect-container.ts | 23 +++- .../collect/compose-collect-container.ts | 23 +++- .../skyflow-flowvault-js/src/index-node.ts | 21 ++- .../src/utils/common/index.ts | 17 ++- .../src/utils/validators/index.ts | 103 ++++++++++++++ .../composable-container.flowdb.test.ts | 31 +++++ .../tests/utils/validators.flowdb.test.ts | 126 ++++++++++++++++++ packages/skyflow-js/jest.config.json | 8 ++ packages/skyflow-js/src/index-node.ts | 19 ++- 14 files changed, 392 insertions(+), 21 deletions(-) create mode 100644 jest.config.js create mode 100644 packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts diff --git a/.eslintignore b/.eslintignore index 179b13e5..9b46d517 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,4 +7,5 @@ tests babel.config.js webpack.* +jest.config.js samples \ No newline at end of file diff --git a/core/external/collect/composable-collect-element.ts b/core/external/collect/composable-collect-element.ts index 2bc492bf..80fe8d73 100644 --- a/core/external/collect/composable-collect-element.ts +++ b/core/external/collect/composable-collect-element.ts @@ -20,7 +20,13 @@ import { } from '@core/types'; import { printLog } from '@core/utils/logs-helper'; -class ComposableElement { +// 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; @@ -99,7 +105,7 @@ class ComposableElement { return this.#elementName; } - update = (options: ICollectElementUpdateOptionsBase) => { + update = (options: TUpdateOptions) => { this.#isUpdateCalled = true; if (this.#isMounted) { options.validations = formatValidations(options.validations); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..147af8b2 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,20 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +// Root Jest config for the monorepo. Runs both packages as Jest `projects` in a +// single invocation and merges their coverage into one report. This is the only +// way to get an honest number for the shared `core/` folder: privacyDB and +// flowDB each exercise different slices of core (file-upload/3DS vs flowDB +// reveal), so a core file's true coverage only exists once both suites' hits are +// merged by absolute path — which the `projects` runner does automatically. +// Per-package `npm test` still works standalone (partial core view); use the +// root `test:coverage` script for the merged, accurate picture. +module.exports = { + projects: [ + '/packages/skyflow-js/jest.config.json', + '/packages/skyflow-flowvault-js/jest.config.json', + ], + collectCoverage: true, + coverageDirectory: '/coverage', + coverageReporters: ['text', 'lcov'], +}; diff --git a/package.json b/package.json index 22757991..8397c8d1 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "build-node-sdk": "npm run build-node-sdk --workspaces --if-present", "build-iframe": "npm run build-iframe --workspaces --if-present", "build": "npm run build-browser-sdk && npm run build-node-sdk && npm run build-iframe", - "test": "npm run test --workspaces --if-present" + "test": "npm run test --workspaces --if-present", + "test:coverage": "jest --config jest.config.js" }, "engines": { "node": ">=12.0", diff --git a/packages/skyflow-flowvault-js/jest.config.json b/packages/skyflow-flowvault-js/jest.config.json index c6e7196b..7ef2f949 100644 --- a/packages/skyflow-flowvault-js/jest.config.json +++ b/packages/skyflow-flowvault-js/jest.config.json @@ -1,6 +1,14 @@ { "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"], diff --git a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts index a4cbca23..305039ef 100644 --- a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts +++ b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts @@ -12,7 +12,11 @@ import CoreCollectContainer, { ICollectElementBase, } from '@core/external/collect/collect-container'; import { VariantCollectAdapter } from '@core/types'; -import { validateCollectElementInput } from '../../utils/validators'; +import { + validateCollectElementInput, + validateFlowDBAdditionalFieldsInCollect, + validateFlowDBUpsertOptions, +} from '../../utils/validators'; import { CollectElementInput, CollectElementOptions, CollectElementUpdateOptions, ICollectOptions, } from '../../utils/common'; @@ -48,12 +52,19 @@ CollectElementInput, CollectElementOptions return { table: input.tableName }; } - // flowDB forces tokens on and does not validate a client-supplied value. - // Reuse the base validation for additionalFields/upsert, then force tokens on - // in the emitted options (flowDB has no client-facing `tokens`). + // 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 { - const validated = super.validateCollectOptions(options); - return { ...validated, tokens: true } as 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 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 index fef97aef..cf328c3f 100644 --- a/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts +++ b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts @@ -13,7 +13,11 @@ import CoreComposableCollectContainer from '@core/external/collect/composable-co import { VariantCollectAdapter } from '@core/types'; import { CollectElementInput, CollectElementOptions, ICollectOptions } from '../../utils/common'; import { CollectResponse } from '../../internal/internal-types'; -import { validateCollectElementInput } from '../../utils/validators'; +import { + validateCollectElementInput, + validateFlowDBAdditionalFieldsInCollect, + validateFlowDBUpsertOptions, +} from '../../utils/validators'; import SkyflowFlowDBError from '../../libs/skyflow-flowdb-error'; import collectVariant from './collect-variant'; @@ -34,12 +38,19 @@ ICollectOptions, CollectResponse, CollectElementInput, CollectElementOptions return { table: input.tableName }; } - // flowDB forces tokens on and does not validate a client-supplied value. - // Reuse the base validation for additionalFields/upsert, then force tokens on - // in the emitted options (flowDB has no client-facing `tokens`). + // 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 { - const validated = super.validateCollectOptions(options); - return { ...validated, tokens: true } as 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 diff --git a/packages/skyflow-flowvault-js/src/index-node.ts b/packages/skyflow-flowvault-js/src/index-node.ts index 07d04cd6..6aa13648 100644 --- a/packages/skyflow-flowvault-js/src/index-node.ts +++ b/packages/skyflow-flowvault-js/src/index-node.ts @@ -7,6 +7,9 @@ Copyright (c) 2025 Skyflow, Inc. // (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 --- @@ -16,6 +19,8 @@ export { CollectElementUpdateOptions, ICollectOptions as CollectOptions, IFlowDBUpsertOptions as UpsertOptions, + AdditionalFields, + AdditionalFieldsRecord, IFlowDBRevealElementInput as RevealElementInput, IRevealElementOptions as RevealElementOptions, IRevealOptions as RevealOptions, @@ -61,10 +66,22 @@ export { } from './skyflow'; // --- element container / element classes --- -export { default as CollectElement } from '@core/external/collect/collect-element'; +// 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; +// 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 ComposableElement } from './external/collect/compose-collect-element'; 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'; diff --git a/packages/skyflow-flowvault-js/src/utils/common/index.ts b/packages/skyflow-flowvault-js/src/utils/common/index.ts index 57f7fec2..1cb76332 100644 --- a/packages/skyflow-flowvault-js/src/utils/common/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/common/index.ts @@ -66,7 +66,6 @@ import type { ICollectElementOptionsBase, ICollectElementUpdateOptionsBase, IElementStateBase, - IInsertRecordInput as IInsertRecordInputType, CollectElementInput as ICoreCollectElementInput, } from '@core/types'; @@ -114,6 +113,20 @@ export interface IFlowDBUpsertOptions { 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 { @@ -140,6 +153,6 @@ export interface IRevealOptions { // per-table `updateType` drives the update variant — there is no top-level // `updateType`. export interface ICollectOptions extends ICollectOptionsBase { - additionalFields?: IInsertRecordInputType; + additionalFields?: AdditionalFields; upsert?: Array; } diff --git a/packages/skyflow-flowvault-js/src/utils/validators/index.ts b/packages/skyflow-flowvault-js/src/utils/validators/index.ts index d3aadd19..50d4697d 100644 --- a/packages/skyflow-flowvault-js/src/utils/validators/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/validators/index.ts @@ -11,6 +11,9 @@ import { MessageType, CollectElementInput, LogLevel, + UpdateType, + IFlowDBUpsertOptions, + AdditionalFields, } from '../common'; import { printLog } from '../logs-helper'; @@ -114,3 +117,103 @@ export const validateCollectElementInput = (input: CollectElementInput, logLevel throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOWID_IN_COLLECT, [], 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/core/external/collect/composable-container.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/core/external/collect/composable-container.flowdb.test.ts index 27b8cfba..b8615e3a 100644 --- 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 @@ -255,4 +255,35 @@ describe('flowDB composable collect container', () => { 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); + }); + }); }); 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 00000000..9bd615a0 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts @@ -0,0 +1,126 @@ +/* + 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, +} from '../../src/utils/validators'; +import { UpdateType } 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('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/); + }); +}); diff --git a/packages/skyflow-js/jest.config.json b/packages/skyflow-js/jest.config.json index c6e7196b..7ef2f949 100644 --- a/packages/skyflow-js/jest.config.json +++ b/packages/skyflow-js/jest.config.json @@ -1,6 +1,14 @@ { "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"], diff --git a/packages/skyflow-js/src/index-node.ts b/packages/skyflow-js/src/index-node.ts index ec2105fa..4d38ce02 100644 --- a/packages/skyflow-js/src/index-node.ts +++ b/packages/skyflow-js/src/index-node.ts @@ -1,6 +1,9 @@ /* 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 { @@ -72,10 +75,22 @@ export type { ISkyflow as SkyflowConfig, } from './skyflow'; -export { default as CollectElement } from '@core/external/collect/collect-element'; +// 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 ComposableElement } from './external/collect/compose-collect-element'; 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'; From 58fd30b19bc1f8e98a4fb3a1e462289874cac0b5 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 18 Aug 2026 17:20:06 +0530 Subject: [PATCH 077/103] SK-3041:Fix metadata object for skyflowContainer. --- core/external/base-skyflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index 9729a998..ff6da1a2 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -321,7 +321,7 @@ abstract class BaseSkyflow< ...this.metadata, clientJSON: this.client.toJSON(), containerType: type, - skyflowContainer: this.skyflowContainer, + skyflowContainer: { isControllerFrameReady: this.skyflowContainer.isControllerFrameReady }, getSkyflowBearerToken: this.getSkyflowBearerToken, }); From 1d4b79e97194870e806faae8489432d6b0b6b494 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 18 Aug 2026 20:50:04 +0530 Subject: [PATCH 078/103] SK-3041:Add returnMockValue in collectElementOptions. --- core/external/base-skyflow.ts | 2 +- core/external/collect/collect-container.ts | 4 ++ core/external/collect/collect-element.ts | 7 +++ .../reveal/composable-reveal-internal.ts | 6 ++ core/internal/frame-element-init.ts | 5 +- core/internal/iframe-form/index.ts | 2 + core/internal/index.ts | 4 ++ .../skyflow-frame/collect-elements.ts | 3 +- .../src/external/collect/collect-container.ts | 7 ++- .../collect/compose-collect-container.ts | 7 ++- .../src/utils/common/index.ts | 4 +- .../src/utils/helpers/index.ts | 37 +++++++----- .../frame-element-init.flowdb.test.js | 59 ++++++++++++++++++- 13 files changed, 123 insertions(+), 24 deletions(-) diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index ff6da1a2..9729a998 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -321,7 +321,7 @@ abstract class BaseSkyflow< ...this.metadata, clientJSON: this.client.toJSON(), containerType: type, - skyflowContainer: { isControllerFrameReady: this.skyflowContainer.isControllerFrameReady }, + skyflowContainer: this.skyflowContainer, getSkyflowBearerToken: this.getSkyflowBearerToken, }); diff --git a/core/external/collect/collect-container.ts b/core/external/collect/collect-container.ts index 55b010e7..3b6f419a 100644 --- a/core/external/collect/collect-container.ts +++ b/core/external/collect/collect-container.ts @@ -244,6 +244,10 @@ abstract class CollectContainer< // active key comes from the registered VariantAdapter. options.skyflowID = element[this.collectVariant.skyflowIdKey]; + options.returnMockValue = element.returnMockValue === true + ? element.returnMockValue + : false; + elements.push(options); }); }); diff --git a/core/external/collect/collect-element.ts b/core/external/collect/collect-element.ts index 47f2f97b..a8eb0adf 100644 --- a/core/external/collect/collect-element.ts +++ b/core/external/collect/collect-element.ts @@ -270,6 +270,13 @@ class CollectElement< } else if (domElement instanceof HTMLElement) { this.resizeObserver?.observe(domElement); } + + this.#metaData = { + ...this.#metaData, + skyflowContainer: { + isControllerFrameReady: this.#metaData.skyflowContainer?.isControllerFrameReady, + }, + }; const isComposable = this.#elements.length > 1; if (isComposable) { this.#iframe.mount(domElement, this.#elementId, { diff --git a/core/external/reveal/composable-reveal-internal.ts b/core/external/reveal/composable-reveal-internal.ts index 54fddbea..9655e758 100644 --- a/core/external/reveal/composable-reveal-internal.ts +++ b/core/external/reveal/composable-reveal-internal.ts @@ -205,6 +205,12 @@ class ComposableRevealInternalElement extends SkyflowElement { } this.#readyToMount = true; + this.metaData = { + ...this.metaData, + skyflowContainer: { + isControllerFrameReady: this.metaData.skyflowContainer?.isControllerFrameReady, + }, + }; if (this.#readyToMount) { this.#iframe.mount(domElementSelector, undefined, { record: JSON.stringify({ diff --git a/core/internal/frame-element-init.ts b/core/internal/frame-element-init.ts index 4bb4e9cf..a59059c4 100644 --- a/core/internal/frame-element-init.ts +++ b/core/internal/frame-element-init.ts @@ -197,7 +197,10 @@ export default abstract class FrameElementInit { !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name ) { - const isCVV = inputElement.fieldType === ELEMENTS.CVV.name; + const isCVV = inputElement.fieldType + === ELEMENTS.CVV.name + && inputElement.returnMockValue === true; + if ( inputElement.fieldType === ELEMENTS.checkbox.name diff --git a/core/internal/iframe-form/index.ts b/core/internal/iframe-form/index.ts index 6da71a07..9312a928 100644 --- a/core/internal/iframe-form/index.ts +++ b/core/internal/iframe-form/index.ts @@ -115,6 +115,8 @@ export default class IFrameFormElement extends EventEmitter { maxFileCount: number = 4; + returnMockValue: boolean = false; + constructor(name: string, label: string, metaData: any, context: Context, skyflowID?: string) { super(); const frameValues = name.split(':'); diff --git a/core/internal/index.ts b/core/internal/index.ts index 3f452a1d..a0e8b6fd 100644 --- a/core/internal/index.ts +++ b/core/internal/index.ts @@ -126,6 +126,10 @@ export default class FrameElement { if (Object.prototype.hasOwnProperty.call(options, 'maxFileCount')) { this.iFrameFormElement.maxFileCount = options?.maxFileCount; } + + if (Object.prototype.hasOwnProperty.call(options, 'returnMockValue')) { + this.iFrameFormElement.returnMockValue = options?.returnMockValue; + } } // mount element onto dom diff --git a/core/internal/skyflow-frame/collect-elements.ts b/core/internal/skyflow-frame/collect-elements.ts index 39dd659c..4d77385f 100644 --- a/core/internal/skyflow-frame/collect-elements.ts +++ b/core/internal/skyflow-frame/collect-elements.ts @@ -131,7 +131,8 @@ export const collectElementsData = ( inputElement.iFrameFormElement.fieldType !== ELEMENTS.FILE_INPUT.name && inputElement.iFrameFormElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name ) { - const isCVV = inputElement.iFrameFormElement.fieldType === ELEMENTS.CVV.name; + const isCVV = inputElement.iFrameFormElement.fieldType === ELEMENTS.CVV.name + && inputElement.iFrameFormElement.returnMockValue === true; if ( inputElement.iFrameFormElement.fieldType === ELEMENTS.checkbox.name ) { diff --git a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts index 305039ef..b5b01267 100644 --- a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts +++ b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts @@ -48,8 +48,11 @@ CollectElementInput, CollectElementOptions // Map the client-facing `tableName` key onto the internal `table` name that the // rest of the collect pipeline consumes. // eslint-disable-next-line class-methods-use-this - protected buildCreateElementFields(input: CollectElementInput): Record { - return { table: input.tableName }; + protected buildCreateElementFields( + input: CollectElementInput, + options: CollectElementOptions, + ): Record { + return { table: input.tableName, ...options }; } // flowDB validates additionalFields/upsert against the flowDB key shapes 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 index cf328c3f..39e3766f 100644 --- a/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts +++ b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts @@ -34,8 +34,11 @@ ICollectOptions, CollectResponse, CollectElementInput, CollectElementOptions // Map the client-facing `tableName` key onto the internal `table` name that the // rest of the collect pipeline consumes. // eslint-disable-next-line class-methods-use-this - protected buildCreateElementFields(input: CollectElementInput): Record { - return { table: input.tableName }; + protected buildCreateElementFields( + input: CollectElementInput, + options: CollectElementOptions, + ): Record { + return { table: input.tableName, ...options }; } // flowDB validates additionalFields/upsert against the flowDB key shapes diff --git a/packages/skyflow-flowvault-js/src/utils/common/index.ts b/packages/skyflow-flowvault-js/src/utils/common/index.ts index 1cb76332..ccae4e6d 100644 --- a/packages/skyflow-flowvault-js/src/utils/common/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/common/index.ts @@ -72,7 +72,9 @@ import type { // 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 {} +export interface CollectElementOptions extends ICollectElementOptionsBase { + returnMockValue?: boolean; +} // flowDB element state: shared base + `value` without `Blob` (no file elements). // See Decision 2.5. diff --git a/packages/skyflow-flowvault-js/src/utils/helpers/index.ts b/packages/skyflow-flowvault-js/src/utils/helpers/index.ts index 309d0e5f..762502ea 100644 --- a/packages/skyflow-flowvault-js/src/utils/helpers/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/helpers/index.ts @@ -40,23 +40,32 @@ export const getDeviceType = metricsHelper.getDeviceType; export const getMetaObject = metricsHelper.getMetaObject; +export const MOCK_CVV_THREE_DIGIT = '817'; + +export const MOCK_CVV_FOUR_DIGIT = '8173'; + // Replaces a captured CVV value with a mock of the same length that never equals // the entered value. Uses the crypto RNG (leading zeros allowed). flowDB-only. -export const generateMockCVV = (length: number, actualValue: string): string => { - if (length <= 0) return ''; - const buildCandidate = () => { - const bytes = crypto.getRandomValues(new Uint8Array(length)); - let candidate = ''; - for (let i = 0; i < length; i += 1) { - candidate += (bytes[i] % 10).toString(); - } - return candidate; - }; - let mock = buildCandidate(); - while (mock === actualValue) { - mock = buildCandidate(); +export const generateMockCVV = (length: number, actualValue?: string): string => { + switch (length) { + case 3: return MOCK_CVV_THREE_DIGIT; + case 4: return MOCK_CVV_FOUR_DIGIT; + default: return ''; } - return mock; + // if (length <= 0) return ''; + // const buildCandidate = () => { + // const bytes = crypto.getRandomValues(new Uint8Array(length)); + // let candidate = ''; + // for (let i = 0; i < length; i += 1) { + // candidate += (bytes[i] % 10).toString(); + // } + // return candidate; + // }; + // let mock = buildCandidate(); + // while (mock === actualValue) { + // mock = buildCandidate(); + // } + // return mock; }; // --- Variant-neutral element helpers (copied verbatim from skyflow-js helpers; 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 index 29b8fc1a..322783d9 100644 --- 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 @@ -111,7 +111,7 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { test('replaces the CVV element token with a 3-digit mock that differs from the entered value, leaving sibling tokens intact', async () => { const instance = new FrameElementInit(); - const cvv = { ...makeTextElement({ name: 'cvv', tableName: 'cards', value: '123' }), fieldType: ELEMENTS.CVV.name }; + 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(() => [ @@ -131,12 +131,67 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { const res = await instance['tokenize']({ options: {} }, config); const cvvToken = res.records[0].tokens.cvv[0].token; expect(cvvToken).toHaveLength(3); - expect(/^[0-9]+$/.test(cvvToken)).toBe(true); + 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 a 4-digit mock that differs from the entered value, 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('should not replace CVV element token with a 4-digit mock that differs from the entered value, 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).not.toHaveLength(4); + expect(cvvToken).not.toEqual('8173'); + expect(cvvToken).not.toEqual('1234'); + expect(cvvToken).toEqual('real-cvv-token'); + 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(); From 22eb4f60f3ef04f81c0eb15316db44a0312137cc Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 18 Aug 2026 21:56:55 +0530 Subject: [PATCH 079/103] SK-3041:Fix skyflowContainer config. --- core/external/collect/collect-element.ts | 6 -- .../reveal/composable-reveal-internal.ts | 6 -- core/external/skyflow-container.ts | 10 +++ .../core/external/skyflow-container.test.ts | 74 +++++++++++++++++++ 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 packages/skyflow-js/tests/core/external/skyflow-container.test.ts diff --git a/core/external/collect/collect-element.ts b/core/external/collect/collect-element.ts index a8eb0adf..478abaf2 100644 --- a/core/external/collect/collect-element.ts +++ b/core/external/collect/collect-element.ts @@ -271,12 +271,6 @@ class CollectElement< this.resizeObserver?.observe(domElement); } - this.#metaData = { - ...this.#metaData, - skyflowContainer: { - isControllerFrameReady: this.#metaData.skyflowContainer?.isControllerFrameReady, - }, - }; const isComposable = this.#elements.length > 1; if (isComposable) { this.#iframe.mount(domElement, this.#elementId, { diff --git a/core/external/reveal/composable-reveal-internal.ts b/core/external/reveal/composable-reveal-internal.ts index 9655e758..54fddbea 100644 --- a/core/external/reveal/composable-reveal-internal.ts +++ b/core/external/reveal/composable-reveal-internal.ts @@ -205,12 +205,6 @@ class ComposableRevealInternalElement extends SkyflowElement { } this.#readyToMount = true; - this.metaData = { - ...this.metaData, - skyflowContainer: { - isControllerFrameReady: this.metaData.skyflowContainer?.isControllerFrameReady, - }, - }; if (this.#readyToMount) { this.#iframe.mount(domElementSelector, undefined, { record: JSON.stringify({ diff --git a/core/external/skyflow-container.ts b/core/external/skyflow-container.ts index acbb69b6..b7819848 100644 --- a/core/external/skyflow-container.ts +++ b/core/external/skyflow-container.ts @@ -64,5 +64,15 @@ class SkyflowContainer { 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/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 00000000..d959eb78 --- /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 }); + }); +}); From f2320885c6c284dc9006714ebe60a4677a258894 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 18 Aug 2026 22:47:32 +0530 Subject: [PATCH 080/103] SK-3041:Update returnMockValue option. --- core/external/collect/collect-container.ts | 4 -- .../src/api-utils/collect.ts | 5 +- .../src/external/collect/collect-container.ts | 8 ++- .../collect/compose-collect-container.ts | 8 ++- .../src/utils/helpers/index.ts | 22 ++------ .../src/utils/validators/index.ts | 14 +++++ .../frame-element-init.flowdb.test.js | 54 ++++++++++++++++--- .../tests/utils/helpers.flowdb.test.ts | 35 ++++++++++++ .../tests/utils/validators.flowdb.test.ts | 20 +++++++ 9 files changed, 136 insertions(+), 34 deletions(-) create mode 100644 packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts diff --git a/core/external/collect/collect-container.ts b/core/external/collect/collect-container.ts index 3b6f419a..55b010e7 100644 --- a/core/external/collect/collect-container.ts +++ b/core/external/collect/collect-container.ts @@ -244,10 +244,6 @@ abstract class CollectContainer< // active key comes from the registered VariantAdapter. options.skyflowID = element[this.collectVariant.skyflowIdKey]; - options.returnMockValue = element.returnMockValue === true - ? element.returnMockValue - : false; - elements.push(options); }); }); diff --git a/packages/skyflow-flowvault-js/src/api-utils/collect.ts b/packages/skyflow-flowvault-js/src/api-utils/collect.ts index aa31a722..2016ec18 100644 --- a/packages/skyflow-flowvault-js/src/api-utils/collect.ts +++ b/packages/skyflow-flowvault-js/src/api-utils/collect.ts @@ -186,9 +186,8 @@ export const replaceCVVTokensInResponse = ( 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 cannot produce a - // value that differs from the empty entered value). - const mock = enteredValue ? generateMockCVV(enteredValue.length, enteredValue) : ''; + // 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) => { diff --git a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts index b5b01267..d201ebd8 100644 --- a/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts +++ b/packages/skyflow-flowvault-js/src/external/collect/collect-container.ts @@ -14,6 +14,7 @@ import CoreCollectContainer, { import { VariantCollectAdapter } from '@core/types'; import { validateCollectElementInput, + validateCollectElementOptions, validateFlowDBAdditionalFieldsInCollect, validateFlowDBUpsertOptions, } from '../../utils/validators'; @@ -46,13 +47,16 @@ CollectElementInput, CollectElementOptions } // Map the client-facing `tableName` key onto the internal `table` name that the - // rest of the collect pipeline consumes. + // 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 { - return { table: input.tableName, ...options }; + validateCollectElementOptions(options); + return { table: input.tableName }; } // flowDB validates additionalFields/upsert against the flowDB key shapes 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 index 39e3766f..c9c39b88 100644 --- a/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts +++ b/packages/skyflow-flowvault-js/src/external/collect/compose-collect-container.ts @@ -15,6 +15,7 @@ import { CollectElementInput, CollectElementOptions, ICollectOptions } from '../ import { CollectResponse } from '../../internal/internal-types'; import { validateCollectElementInput, + validateCollectElementOptions, validateFlowDBAdditionalFieldsInCollect, validateFlowDBUpsertOptions, } from '../../utils/validators'; @@ -32,13 +33,16 @@ ICollectOptions, CollectResponse, CollectElementInput, CollectElementOptions } // Map the client-facing `tableName` key onto the internal `table` name that the - // rest of the collect pipeline consumes. + // 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 { - return { table: input.tableName, ...options }; + validateCollectElementOptions(options); + return { table: input.tableName }; } // flowDB validates additionalFields/upsert against the flowDB key shapes diff --git a/packages/skyflow-flowvault-js/src/utils/helpers/index.ts b/packages/skyflow-flowvault-js/src/utils/helpers/index.ts index 762502ea..8acf6b8c 100644 --- a/packages/skyflow-flowvault-js/src/utils/helpers/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/helpers/index.ts @@ -44,28 +44,16 @@ export const MOCK_CVV_THREE_DIGIT = '817'; export const MOCK_CVV_FOUR_DIGIT = '8173'; -// Replaces a captured CVV value with a mock of the same length that never equals -// the entered value. Uses the crypto RNG (leading zeros allowed). flowDB-only. -export const generateMockCVV = (length: number, actualValue?: string): string => { +// 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 ''; } - // if (length <= 0) return ''; - // const buildCandidate = () => { - // const bytes = crypto.getRandomValues(new Uint8Array(length)); - // let candidate = ''; - // for (let i = 0; i < length; i += 1) { - // candidate += (bytes[i] % 10).toString(); - // } - // return candidate; - // }; - // let mock = buildCandidate(); - // while (mock === actualValue) { - // mock = buildCandidate(); - // } - // return mock; }; // --- Variant-neutral element helpers (copied verbatim from skyflow-js helpers; diff --git a/packages/skyflow-flowvault-js/src/utils/validators/index.ts b/packages/skyflow-flowvault-js/src/utils/validators/index.ts index 50d4697d..b47a3aa9 100644 --- a/packages/skyflow-flowvault-js/src/utils/validators/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/validators/index.ts @@ -10,6 +10,7 @@ import { IFlowDBRevealElementInput as IRevealElementInput, MessageType, CollectElementInput, + CollectElementOptions, LogLevel, UpdateType, IFlowDBUpsertOptions, @@ -118,6 +119,19 @@ export const validateCollectElementInput = (input: CollectElementInput, logLevel } }; +// 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 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 index 322783d9..31d125d8 100644 --- 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 @@ -109,7 +109,7 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { expect(updateDataInCollectFlowDB).not.toHaveBeenCalled(); }); - test('replaces the CVV element token with a 3-digit mock that differs from the entered value, leaving sibling tokens intact', async () => { + 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' }); @@ -137,7 +137,7 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); }); - test('replaces the CVV element token with a 4-digit mock that differs from the entered value, leaving sibling tokens intact', async () => { + 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' }); @@ -165,7 +165,7 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { expect(res.records[0].tokens.card_number[0].token).toEqual('real-card-token'); }); - test('should not replace CVV element token with a 4-digit mock that differs from the entered value, when returnMockValue is false', async () => { + 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' }); @@ -186,12 +186,54 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { }); const res = await instance['tokenize']({ options: {} }, config); const cvvToken = res.records[0].tokens.cvv[0].token; - expect(cvvToken).not.toHaveLength(4); - expect(cvvToken).not.toEqual('8173'); - expect(cvvToken).not.toEqual('1234'); 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(); 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 00000000..7fea4129 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts @@ -0,0 +1,35 @@ +/* + 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, +} from '../../src/utils/helpers'; + +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(''); + }); +}); diff --git a/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts index 9bd615a0..16c535e6 100644 --- a/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts +++ b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts @@ -13,6 +13,7 @@ import SkyflowError from '@core/errors'; import { validateFlowDBUpsertOptions, validateFlowDBAdditionalFieldsInCollect, + validateCollectElementOptions, } from '../../src/utils/validators'; import { UpdateType } from '../../src/utils/common'; @@ -124,3 +125,22 @@ describe('validateFlowDBAdditionalFieldsInCollect', () => { })).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); + }); +}); From f7f0d641b8c1b587042a642fbd49d29825890e1c Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 18 Aug 2026 23:56:02 +0530 Subject: [PATCH 081/103] SK-3041:Minor cleanups. --- core/constants.ts | 5 +++- core/external/base-skyflow.ts | 4 ++-- core/external/collect/collect-container.ts | 5 +++- core/external/reveal/reveal-container.ts | 5 +++- core/internal/frame-element-init.ts | 23 ++++--------------- .../frame-element-init.additional.test.js | 11 +++++---- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/core/constants.ts b/core/constants.ts index 39156b0e..9fadf350 100644 --- a/core/constants.ts +++ b/core/constants.ts @@ -19,7 +19,10 @@ 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'; diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index 9729a998..8be5aac9 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -39,7 +39,7 @@ import { CardType, ElementType, ELEMENT_EVENTS_TO_IFRAME, - SDK_VERSION, + SDK_VERSION_KEY, SESSION_ID, } from '@core/constants'; import properties from '@core/properties'; @@ -147,7 +147,7 @@ abstract class BaseSkyflow< constructor(config: ISkyflow) { const localSDKversion = localStorage.getItem('sdk_version') || ''; - this.metadata[SDK_VERSION] = localSDKversion; + this.metadata[SDK_VERSION_KEY] = localSDKversion; this.metadata[SESSION_ID] = uuid(); this.client = new Client( { diff --git a/core/external/collect/collect-container.ts b/core/external/collect/collect-container.ts index 55b010e7..98973614 100644 --- a/core/external/collect/collect-container.ts +++ b/core/external/collect/collect-container.ts @@ -192,8 +192,11 @@ abstract class CollectContainer< elements: [{ elementType: input.type, name: input.column, - ...input, + // 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, }], diff --git a/core/external/reveal/reveal-container.ts b/core/external/reveal/reveal-container.ts index 477b9d58..498f4e73 100644 --- a/core/external/reveal/reveal-container.ts +++ b/core/external/reveal/reveal-container.ts @@ -276,7 +276,10 @@ abstract class RevealContainer< records: this.#revealRecords, containerId: this.#containerId, errorMessages: this.#customErrorMessages, - options, + // 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 = []; diff --git a/core/internal/frame-element-init.ts b/core/internal/frame-element-init.ts index a59059c4..7531b1da 100644 --- a/core/internal/frame-element-init.ts +++ b/core/internal/frame-element-init.ts @@ -32,7 +32,6 @@ import SkyflowError from '@core/errors'; import { getValueAndItsUnit, validateAndSetupGroupOptions } from '@core/libs/element-options'; import IFrameFormElement from '@core/internal/iframe-form'; import FrameElement from '@core/internal'; -import Client from '@core/client'; import { ContainerType, Context, CVVMap, Env, ErrorType, LogLevel, } from '@core/types'; @@ -58,10 +57,6 @@ export default abstract class FrameElementInit { iframeFormList: IFrameFormElement[] = []; - // Set from the COMPOSABLE_CONTAINER handshake only. The request tail builds its - // own client from the per-request clientConfig, so this is not read there. - #client!: Client; - constructor() { // this.createIframeElement(frameName, label, skyflowID, isRequired); this.context = { logLevel: LogLevel.INFO, env: Env.PROD }; // client level @@ -73,14 +68,13 @@ export default abstract class FrameElementInit { }; this.updateGroupData(); this.createContainerDiv(this.group); + // Handshake with the composable controller frame. The emit itself signals the + // parent container (which flips isComposableFrameReady); the frame's own client + // is built per-request from clientConfig in dispatchCollectRequest, so no reply + // is consumed here. 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; - }); + .emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}); window.addEventListener('message', this.handleCollectCall); } @@ -132,13 +126,6 @@ export default abstract class FrameElementInit { this.handleFileUploadRequest(event); } } - 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; - } } }; diff --git a/packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js b/packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js index 60263a7a..273a2b43 100644 --- a/packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js +++ b/packages/skyflow-js/tests/core/internal/frame-element-init.additional.test.js @@ -798,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) ===== From dcde814350dee0923808716fb63700b6c69d6816 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 00:17:10 +0530 Subject: [PATCH 082/103] SK-3041:SkyflowElement Map cleanup due to no usage & readers. --- core/external/base-skyflow.ts | 15 -- core/external/collect/collect-container.ts | 6 - .../collect/composable-collect-container.ts | 1 - core/external/common/composable-container.ts | 4 - .../reveal/composable-reveal-container.ts | 1 - core/external/reveal/reveal-container.ts | 6 - packages/skyflow-flowvault-js/src/skyflow.ts | 13 +- .../composable-container.flowdb.test.ts | 16 +- packages/skyflow-js/src/skyflow.ts | 13 +- .../collect/collect-container.test.js | 148 +++++++++--------- .../collect/collect-container.test.ts | 14 +- .../collect/composable-container.test.js | 49 +++--- .../collect/composable-container.test.ts | 23 ++- .../reveal-composable-container.test.js | 54 +++---- .../external/reveal/reveal-container.test.js | 30 ++-- .../external/reveal/reveal-container.test.ts | 18 +-- 16 files changed, 182 insertions(+), 229 deletions(-) diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index 8be5aac9..97f49eaf 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -74,7 +74,6 @@ import { IRevealOptionsBase, IRevealResponseBase, ISkyflow, - ISkyflowElement, LogLevel, MessageType, RedactionType, @@ -140,11 +139,6 @@ abstract class BaseSkyflow< protected env: Env; - // The element registry threaded into every container. It is keyed by element - // uuid (never used as a positional array), so the honest shape is a map of the - // shared ISkyflowElement contract — the same type the container bases now take. - protected skyflowElements: Record; - constructor(config: ISkyflow) { const localSDKversion = localStorage.getItem('sdk_version') || ''; this.metadata[SDK_VERSION_KEY] = localSDKversion; @@ -157,7 +151,6 @@ abstract class BaseSkyflow< ); this.logLevel = config?.options?.logLevel || LogLevel.ERROR; this.env = config?.options?.env || Env.PROD; - this.skyflowElements = {}; // 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 @@ -289,28 +282,24 @@ abstract class BaseSkyflow< protected abstract createCollectContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): TCollectContainer; protected abstract createRevealContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): TRevealContainer; protected abstract createComposableContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options: ContainerOptions, ): TComposableContainer; protected abstract createComposeRevealContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): TComposeRevealContainer; @@ -338,7 +327,6 @@ abstract class BaseSkyflow< case ContainerType.COLLECT: { const collectContainer = this.createCollectContainer( this.#containerProps(type), - this.skyflowElements, this.#context(), options, ); @@ -350,7 +338,6 @@ abstract class BaseSkyflow< case ContainerType.REVEAL: { const revealContainer = this.createRevealContainer( this.#containerProps(type), - this.skyflowElements, this.#context(), options, ); @@ -363,7 +350,6 @@ abstract class BaseSkyflow< validateComposableContainerOptions(options!); const composableContainer = this.createComposableContainer( this.#containerProps(type), - this.skyflowElements, this.#context(), options!, ); @@ -377,7 +363,6 @@ abstract class BaseSkyflow< validateComposableContainerOptions(options!); const revealComposableContainer = this.createComposeRevealContainer( this.#containerProps(type), - this.skyflowElements, this.#context(), options, ); diff --git a/core/external/collect/collect-container.ts b/core/external/collect/collect-container.ts index 98973614..01fae4db 100644 --- a/core/external/collect/collect-container.ts +++ b/core/external/collect/collect-container.ts @@ -31,7 +31,6 @@ import { ContainerOptions, ErrorType, ICoreMetadata, - ISkyflowElement, ICollectOptionsBase, ICollectResponseBase, ICollectElementUpdateOptionsBase, @@ -120,8 +119,6 @@ abstract class CollectContainer< protected context: Context; - #skyflowElements: Record; - type:string = ContainerType.COLLECT; #eventEmitter: EventEmitter; @@ -134,7 +131,6 @@ abstract class CollectContainer< constructor( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ) { @@ -154,7 +150,6 @@ abstract class CollectContainer< }, }, }; - this.#skyflowElements = skyflowElements; this.context = context; this.#eventEmitter = new EventEmitter(); @@ -289,7 +284,6 @@ abstract class CollectContainer< this.#eventEmitter, ); this.elements[tempElements.elementName] = element; - this.#skyflowElements[elementId] = element; } if (!isSingleElementAPI) { diff --git a/core/external/collect/composable-collect-container.ts b/core/external/collect/composable-collect-container.ts index 8b2ed7f6..edf4bfd3 100644 --- a/core/external/collect/composable-collect-container.ts +++ b/core/external/collect/composable-collect-container.ts @@ -182,7 +182,6 @@ abstract class CoreComposableCollectContainer< this.eventEmitter, ); this.elements[this.tempElements.elementName] = element; - this.skyflowElements[elementId] = element; } return element; }; diff --git a/core/external/common/composable-container.ts b/core/external/common/composable-container.ts index c2bd3215..b777f6f9 100644 --- a/core/external/common/composable-container.ts +++ b/core/external/common/composable-container.ts @@ -56,8 +56,6 @@ abstract class ComposableContainerBase< protected context: Context; - protected skyflowElements: Record; - protected eventEmitter: EventEmitter; protected isMounted: boolean = false; @@ -102,7 +100,6 @@ abstract class ComposableContainerBase< constructor( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ) { @@ -122,7 +119,6 @@ abstract class ComposableContainerBase< }, }; this.getSkyflowBearerToken = metaData?.getSkyflowBearerToken; - this.skyflowElements = skyflowElements; 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. diff --git a/core/external/reveal/composable-reveal-container.ts b/core/external/reveal/composable-reveal-container.ts index 9fb26424..d25739ec 100644 --- a/core/external/reveal/composable-reveal-container.ts +++ b/core/external/reveal/composable-reveal-container.ts @@ -105,7 +105,6 @@ abstract class CoreComposableRevealContainer< try { element = this.instantiateInternalElement(elementId, this.tempElements); this.elements[this.tempElements.elementName] = element; - this.skyflowElements[elementId] = element; } catch (error: any) { printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, MessageType.ERROR, diff --git a/core/external/reveal/reveal-container.ts b/core/external/reveal/reveal-container.ts index 498f4e73..0283186f 100644 --- a/core/external/reveal/reveal-container.ts +++ b/core/external/reveal/reveal-container.ts @@ -16,7 +16,6 @@ import properties from '@core/properties'; import { ContainerType, IRevealElementOptions, ContainerOptions, Context, ErrorType, MessageType, IRevealResponseBase, ICoreMetadata, RevealContainerProps, - ISkyflowElement, } from '@core/types'; import Container from '@core/external/common/container'; import SkyflowError from '@core/errors'; @@ -58,8 +57,6 @@ abstract class RevealContainer< #context: Context; - #skyflowElements: Record; - #isMounted: boolean = false; type:string = ContainerType.REVEAL; @@ -70,7 +67,6 @@ abstract class RevealContainer< constructor( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ) { @@ -89,7 +85,6 @@ abstract class RevealContainer< }, }, }; - this.#skyflowElements = skyflowElements; this.#containerId = uuid(); this.#eventEmmiter = new EventEmitter(); this.#context = context; @@ -151,7 +146,6 @@ abstract class RevealContainer< }, elementId, this.#context, ); this.#revealElements.push(revealElement); - this.#skyflowElements[elementId] = revealElement; return revealElement; } diff --git a/packages/skyflow-flowvault-js/src/skyflow.ts b/packages/skyflow-flowvault-js/src/skyflow.ts index dd3a3a14..9ff43bdf 100644 --- a/packages/skyflow-flowvault-js/src/skyflow.ts +++ b/packages/skyflow-flowvault-js/src/skyflow.ts @@ -23,7 +23,6 @@ import { Context, ICoreMetadata, ISkyflow, - ISkyflowElement, SkyflowConfigOptions, } from '@core/types'; import RevealContainer from './external/reveal/reveal-container'; @@ -58,41 +57,37 @@ ComposableRevealContainer // eslint-disable-next-line class-methods-use-this protected createCollectContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): CollectContainer { - return new CollectContainer(metaData, skyflowElements, context, options); + return new CollectContainer(metaData, context, options); } // eslint-disable-next-line class-methods-use-this protected createRevealContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): RevealContainer { - return new RevealContainer(metaData, skyflowElements, context, options); + return new RevealContainer(metaData, context, options); } // eslint-disable-next-line class-methods-use-this protected createComposableContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options: ContainerOptions, ): ComposableContainer { - return new ComposableContainer(metaData, skyflowElements, context, options); + return new ComposableContainer(metaData, context, options); } // eslint-disable-next-line class-methods-use-this protected createComposeRevealContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): ComposableRevealContainer { - return new ComposableRevealContainer(metaData, skyflowElements, context, options); + return new ComposableRevealContainer(metaData, context, options); } // ---- Package-specific statics (the rest are inherited from BaseSkyflow) ---- 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 index b8615e3a..dbb102f2 100644 --- 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 @@ -168,18 +168,18 @@ describe('flowDB composable collect container', () => { }); it('constructs a ComposableContainer', () => { - const container = new ComposableContainer(metaData, [], context, { layout: [1] }); + const container = new ComposableContainer(metaData, context, { layout: [1] }); expect(container).toBeInstanceOf(ComposableContainer); }); it('create() returns a ComposableElement for a flowDB (tableName) input', () => { - const container = new ComposableContainer(metaData, [], context, { layout: [1] }); + const container = new ComposableContainer(metaData, context, { layout: [1] }); const element = container.create(cvvElementInput); expect(element).toBeInstanceOf(ComposableElement); }); it('collect() rejects a @core SkyflowError when no elements are added', (done) => { - const container = new ComposableContainer(metaData, [], context, { layout: [1] }); + 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); @@ -188,7 +188,7 @@ describe('flowDB composable collect container', () => { }); it('collect() rejects COMPOSABLE_CONTAINER_NOT_MOUNTED before mount', (done) => { - const container = new ComposableContainer(metaData, [], context, { + const container = new ComposableContainer(metaData, context, { layout: [2], styles: { base: { width: '100px' } }, }); container.create(cvvElementInput); @@ -203,7 +203,7 @@ describe('flowDB composable collect container', () => { 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' } }, }); container.create(cvvElementInput); @@ -226,7 +226,7 @@ describe('flowDB composable collect container', () => { 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' } }, }); container.create(cvvElementInput); @@ -248,7 +248,7 @@ describe('flowDB composable collect container', () => { 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] }); container.create(cvvElementInput); container.create(cardNumberElement); container.mount('#composable'); @@ -261,7 +261,7 @@ describe('flowDB composable collect container', () => { // 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 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', () => { diff --git a/packages/skyflow-js/src/skyflow.ts b/packages/skyflow-js/src/skyflow.ts index 565fa123..e9ea584c 100644 --- a/packages/skyflow-js/src/skyflow.ts +++ b/packages/skyflow-js/src/skyflow.ts @@ -18,7 +18,6 @@ import { Context, ICoreMetadata, ISkyflow, - ISkyflowElement, SkyflowConfigOptions, } from '@core/types'; import logs from '@core/utils/logs'; @@ -74,41 +73,37 @@ ComposableRevealContainer // eslint-disable-next-line class-methods-use-this protected createCollectContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): CollectContainer { - return new CollectContainer(metaData, skyflowElements, context, options); + return new CollectContainer(metaData, context, options); } // eslint-disable-next-line class-methods-use-this protected createRevealContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): RevealContainer { - return new RevealContainer(metaData, skyflowElements, context, options); + return new RevealContainer(metaData, context, options); } // eslint-disable-next-line class-methods-use-this protected createComposableContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options: ContainerOptions, ): ComposableContainer { - return new ComposableContainer(metaData, skyflowElements, context, options); + return new ComposableContainer(metaData, context, options); } // eslint-disable-next-line class-methods-use-this protected createComposeRevealContainer( metaData: ICoreMetadata, - skyflowElements: Record, context: Context, options?: ContainerOptions, ): ComposableRevealContainer { - return new ComposableRevealContainer(metaData, skyflowElements, context, options); + return new ComposableRevealContainer(metaData, context, options); } // ---- privacyDB-only pure-JS API ------------------------------------------- diff --git a/packages/skyflow-js/tests/core/external/collect/collect-container.test.js b/packages/skyflow-js/tests/core/external/collect/collect-container.test.js index e98bed5a..d7e692cf 100644 --- a/packages/skyflow-js/tests/core/external/collect/collect-container.test.js +++ b/packages/skyflow-js/tests/core/external/collect/collect-container.test.js @@ -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/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts b/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts index d5352066..34d95ce0 100644 --- a/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts @@ -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/packages/skyflow-js/tests/core/external/collect/composable-container.test.js b/packages/skyflow-js/tests/core/external/collect/composable-container.test.js index c04afdc5..5c2df6d4 100644 --- a/packages/skyflow-js/tests/core/external/collect/composable-container.test.js +++ b/packages/skyflow-js/tests/core/external/collect/composable-container.test.js @@ -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/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts b/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts index 7a2d5612..a1747a68 100644 --- a/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts @@ -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/packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js index 4cc9bb83..48e471ac 100644 --- a/packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-composable-container.test.js @@ -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/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js index cd39c538..caeee6f7 100644 --- a/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.js @@ -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/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts index 8c49e6cd..f89524bc 100644 --- a/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-container.test.ts @@ -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, }); From b250bd248800e6b637ec6627e2c3863e22286488 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 00:52:28 +0530 Subject: [PATCH 083/103] SK-3041:add cvv gate & removed unused emitter. --- core/internal/frame-element-init.ts | 8 ++++- core/utils/bus-events/index.ts | 14 +++------ .../src/internal/frame-element-init.ts | 3 ++ .../skyflow-js/tests/utils/bus-events.test.js | 29 ------------------- 4 files changed, 14 insertions(+), 40 deletions(-) diff --git a/core/internal/frame-element-init.ts b/core/internal/frame-element-init.ts index 7531b1da..f908f399 100644 --- a/core/internal/frame-element-init.ts +++ b/core/internal/frame-element-init.ts @@ -81,6 +81,11 @@ export default abstract class FrameElementInit { // ---- Injected divergence (bound per package in the subclass) ---- + // flowDB captures CVV values into `cvvMap` (later used to mask CVV tokens in the + // response); privacyDB does not. Mirrors `collectsCVV` on the skyflow-frame + // controller base so both collect paths gate CVV capture the same way. + protected collectsCVV: boolean = false; + // The variant request tail: build the transport request from the assembled // insert/update objects, dispatch it, and shape the response. privacyDB and // flowDB implement this against their own api-utils/collect. @@ -184,7 +189,8 @@ export default abstract class FrameElementInit { !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name ) { - const isCVV = inputElement.fieldType + const isCVV = this.collectsCVV + && inputElement.fieldType === ELEMENTS.CVV.name && inputElement.returnMockValue === true; diff --git a/core/utils/bus-events/index.ts b/core/utils/bus-events/index.ts index e1c2b66b..01aa6ade 100644 --- a/core/utils/bus-events/index.ts +++ b/core/utils/bus-events/index.ts @@ -7,6 +7,10 @@ import properties from '@core/properties'; export function getAccessToken(clientId: string) { return new Promise((resolve, reject) => { + // The bearer-token channel is namespaced per Skyflow instance by `clientId` + // (the parent's uuid) so concurrent SDK instances on one page don't clash. + // The listener lives on `GET_BEARER_TOKEN + uuid` (base-skyflow); there is no + // un-namespaced listener, so only this suffixed emit is dispatched. bus // .target(properties.IFRAME_SECURE_ORIGIN) .emit(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN + clientId, {}, @@ -16,16 +20,6 @@ export function getAccessToken(clientId: string) { } resolve(data.authToken); }); - - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN, {}, - (data:any) => { - if (data?.error) { - reject(data.error); - } - resolve(data.authToken); - }); }); } diff --git a/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts b/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts index 7b1a2a44..fb977606 100644 --- a/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts +++ b/packages/skyflow-flowvault-js/src/internal/frame-element-init.ts @@ -22,6 +22,9 @@ import { 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(); }; diff --git a/packages/skyflow-js/tests/utils/bus-events.test.js b/packages/skyflow-js/tests/utils/bus-events.test.js index b43586ff..48999bb0 100644 --- a/packages/skyflow-js/tests/utils/bus-events.test.js +++ b/packages/skyflow-js/tests/utils/bus-events.test.js @@ -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(); - }); - }); }); From 255d581c0b2a21afc2f967164d7df7825b9636f3 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 01:15:18 +0530 Subject: [PATCH 084/103] SK-3041:Fix sdk version log telmetry leak. --- core/external/base-skyflow.ts | 4 +-- core/utils/logs-helper/index.ts | 18 +++++++++++- .../tests/utils/logs-helper.flowdb.test.js | 29 +++++++++++++++++++ .../tests/utils/logs-helper.test.js | 28 ++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 packages/skyflow-flowvault-js/tests/utils/logs-helper.flowdb.test.js create mode 100644 packages/skyflow-js/tests/utils/logs-helper.test.js diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index 97f49eaf..de283486 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -55,7 +55,7 @@ import CoreComposableRevealContainer from '@core/external/reveal/composable-reve import type CoreRevealElement from '@core/external/reveal/reveal-element'; import { checkAndSetForCustomUrl, formatVaultURL } from '@core/helpers'; import { validateComposableContainerOptions } from '@core/validators'; -import { printLog, parameterizedString } from '@core/utils/logs-helper'; +import { printLog, parameterizedString, getStoredSdkVersion } from '@core/utils/logs-helper'; import { ClientMetadata, CollectElementInput, @@ -140,7 +140,7 @@ abstract class BaseSkyflow< protected env: Env; constructor(config: ISkyflow) { - const localSDKversion = localStorage.getItem('sdk_version') || ''; + const localSDKversion = getStoredSdkVersion(); this.metadata[SDK_VERSION_KEY] = localSDKversion; this.metadata[SESSION_ID] = uuid(); this.client = new Client( diff --git a/core/utils/logs-helper/index.ts b/core/utils/logs-helper/index.ts index 6d518ef3..99846b79 100644 --- a/core/utils/logs-helper/index.ts +++ b/core/utils/logs-helper/index.ts @@ -57,12 +57,28 @@ export const getElementName = (name:string = '') => { const SDK_OWNER = '[Skyflow]'; +// Resolve the optional `name@version` telemetry override kept in localStorage. +// The un-namespaced `sdk_version` key is written by skyflow-react-js, which wraps +// skyflow-js only; reading it from any other bundle (e.g. skyflow-flowvault-js) +// would leak skyflow-js's React identity into that bundle's sky-metadata header +// and error-log prefix. Prefer a per-package namespaced key first (a future React +// wrapper writes `sdk_version:`), then fall back to the legacy global key +// ONLY for skyflow-js so its already-shipped React wrapper keeps working. +export const getStoredSdkVersion = (): string => { + const namespaced = localStorage.getItem(`sdk_version:${SDK_NAME}`); + if (namespaced) return namespaced; + if (SDK_NAME === 'skyflow-js') { + return localStorage.getItem('sdk_version') || ''; + } + return ''; +}; + // Resolve this bundle's SDK language/version label for the error log line. // Mirrors each package's former `getSDKLanguageAndVersion`: reads the injected // `SDK_NAME`/`SDK_VERSION` globals, honouring an optional `name@version` // override stored in localStorage (used by the React wrapper). const getSdkLanguageAndVersion = () => { - const metaData = localStorage.getItem('sdk_version') || ''; + const metaData = getStoredSdkVersion(); let sdkName = SDK_NAME; let sdkVersion = SDK_VERSION; if (metaData && metaData !== '' && metaData.split('@').length > 1) { 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 00000000..5f431309 --- /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-js/tests/utils/logs-helper.test.js b/packages/skyflow-js/tests/utils/logs-helper.test.js new file mode 100644 index 00000000..83c7d2ce --- /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'); + }); +}); From ee108a2934f3d8e7783721f9ee6e602a33d41ee3 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 09:51:00 +0530 Subject: [PATCH 085/103] SK-3041:Fix httpCode as per proto. --- packages/skyflow-flowvault-js/src/api-utils/collect.ts | 4 ++-- .../src/internal/internal-types/index.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/skyflow-flowvault-js/src/api-utils/collect.ts b/packages/skyflow-flowvault-js/src/api-utils/collect.ts index 2016ec18..28eeb028 100644 --- a/packages/skyflow-flowvault-js/src/api-utils/collect.ts +++ b/packages/skyflow-flowvault-js/src/api-utils/collect.ts @@ -127,7 +127,7 @@ export const constructFlowDBInsertResponse = ( records.push({ error: res.error, tableName: res.tableName, - httpCode: res.httpCode as number, + httpCode: res.httpCode, }); return; } @@ -137,7 +137,7 @@ export const constructFlowDBInsertResponse = ( ...(res.skyflowID ? { skyflowId: res.skyflowID } : {}), tokens: res.tokens ?? {}, ...(hasHashedData ? { hashedData: res.hashedData } : {}), - httpCode: res.httpCode as number, + httpCode: res.httpCode, }); }); diff --git a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts index 4a18fa5c..09d24d9c 100644 --- a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts +++ b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts @@ -95,7 +95,9 @@ export interface FlowDBRecordResponse { data?: Record; hashedData?: Record; error?: string | null; - httpCode?: number; + // 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; } @@ -210,7 +212,10 @@ export interface RevealRecord { token: string; tokenGroupName?: string; metadata?: RevealRecordMetadata; - httpCode: number; + // 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; } From a532511c90b77c55007e68e1855944613f9e2037 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 12:54:24 +0530 Subject: [PATCH 086/103] SK-3041:Handle collect partial error case with insert & update. --- .../src/api-utils/collect.ts | 37 +++++++++ .../src/internal/internal-types/index.ts | 7 +- .../skyflow-frame/skyflow-frame-controller.ts | 20 ++--- .../tests/api-utils/collect.flowdb.test.js | 83 +++++++++++++++++++ 4 files changed, 135 insertions(+), 12 deletions(-) diff --git a/packages/skyflow-flowvault-js/src/api-utils/collect.ts b/packages/skyflow-flowvault-js/src/api-utils/collect.ts index 28eeb028..c4a9d48c 100644 --- a/packages/skyflow-flowvault-js/src/api-utils/collect.ts +++ b/packages/skyflow-flowvault-js/src/api-utils/collect.ts @@ -210,6 +210,43 @@ export const replaceCVVTokensInResponse = ( 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) { diff --git a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts index 09d24d9c..b154b63a 100644 --- a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts +++ b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts @@ -131,7 +131,12 @@ export interface CollectRecord { skyflowId?: string; tokens?: Record; hashedData?: Record; - httpCode: number; + // 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; } 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 index b3d99131..c3d18877 100644 --- 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 @@ -21,7 +21,7 @@ import { constructFlowDBUpdateRequest, insertDataInCollectFlowDB, updateDataInCollectFlowDB, - replaceCVVTokensInResponse, + mergeFlowDBCollectResponses, } from '../../api-utils/collect'; import { fetchRecordsByTokenIdFlowDB, @@ -128,17 +128,15 @@ class SkyflowFrameController return; } Promise.all(requests).then((responses: any[]) => { - const failure = responses.find((response) => response?.error !== undefined); - if (failure) { - rootReject(failure); - return; + // 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); } - const records = responses.reduce( - (acc, response) => acc.concat(response?.records || []), - [] as any[], - ); - replaceCVVTokensInResponse(records, cvvMap); - rootResolve({ records }); }); }).catch((err) => { rootReject(err); 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 index e15e2b1c..95a212cc 100644 --- a/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js +++ b/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js @@ -7,6 +7,7 @@ import { constructFlowDBUpdateRequest, insertDataInCollectFlowDB, updateDataInCollectFlowDB, + mergeFlowDBCollectResponses, } from '../../src/api-utils/collect'; // Note: the flowvault collect data layer receives the auth token as a parameter @@ -369,3 +370,85 @@ describe('updateDataInCollectFlowDB', () => { ]); }); }); + +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 }); + }); +}); From c50d1c3f525eefc0ab1c05bb269a1fb3ebe712b4 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 14:52:39 +0530 Subject: [PATCH 087/103] SK-3041:Disable logs for pureJS listeners in flowDB. --- .../skyflow-frame-controller-base.ts | 19 +++++++++++++++---- .../skyflow-frame/skyflow-frame-controller.ts | 4 ++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/internal/skyflow-frame/skyflow-frame-controller-base.ts b/core/internal/skyflow-frame/skyflow-frame-controller-base.ts index 267d7a2b..be64cd10 100644 --- a/core/internal/skyflow-frame/skyflow-frame-controller-base.ts +++ b/core/internal/skyflow-frame/skyflow-frame-controller-base.ts @@ -74,6 +74,12 @@ abstract class CoreSkyflowFrameController< // privacyDB always rejects the reveal error branch. protected revealResolvesPartialFailure: boolean = false; + // privacyDB overrides to `true`: it registers the pure-JS data-access channel + // (DETOKENIZE/INSERT/UPDATE/GET/DELETE) and so announces those listeners in the + // readiness handshake. flowDB is elements-only — it registers no such channel, + // so it must not advertise listeners it never wired. + protected registersDataAccessListeners: boolean = false; + constructor(clientId: string) { this.clientId = clientId || ''; const encodedClientDomain = getValueFromName(window.name, 2); @@ -220,10 +226,15 @@ abstract class CoreSkyflowFrameController< ...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); - }); + // Only announce the pure-JS listeners when this variant actually registers + // the channel (privacyDB). flowDB leaves registerDataAccessListeners a + // no-op, so it must not log listeners that were never wired. + if (this.registersDataAccessListeners) { + Object.keys(PUREJS_TYPES).forEach((key) => { + printLog(parameterizedString(logs.infoLogs.LISTEN_PURE_JS_REQUEST, + CLASS_NAME, PUREJS_TYPES[key]), MessageType.LOG, this.context.logLevel); + }); + } }); } 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 index 4119c2b1..ca473e1c 100644 --- a/packages/skyflow-js/src/internal/skyflow-frame/skyflow-frame-controller.ts +++ b/packages/skyflow-js/src/internal/skyflow-frame/skyflow-frame-controller.ts @@ -74,6 +74,10 @@ import { 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); From e5a69a7e0a5e0b4a7c70a8d8b8578ef31b7820d9 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 16:06:23 +0530 Subject: [PATCH 088/103] SK-3041:Add cross release triggers guards. --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13d6ad02..a11e3acd 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" From 04938174ed7162dce6c24b28b80f098f07a0001c Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 17:25:40 +0530 Subject: [PATCH 089/103] SK-3041:Update internal release cache invalidation path for flowvault. --- .github/workflows/internal_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/internal_release.yml b/.github/workflows/internal_release.yml index a018b9cf..e8a1eb7e 100644 --- a/.github/workflows/internal_release.yml +++ b/.github/workflows/internal_release.yml @@ -94,7 +94,7 @@ jobs: DIST_DIR: "packages/skyflow-flowvault-js/dist/v1" S3_PREFIX: "flowvault/" TAG_PREFIX: "flowvault-" - INVALIDATION_PATH: "/flowvault/*" + INVALIDATION_PATH: "/*" secrets: # Same internal/BLITZ bucket + distribution + CDN origin as skyflow-js; # flowDB just lands under the flowvault/ key prefix. From 033497e47840df7c8368ef600d40972ed3f4891d Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 17:43:22 +0530 Subject: [PATCH 090/103] SK-3041:Update .eslintignore file for webpack directory. --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 9b46d517..96ea5f9a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,5 +7,6 @@ tests babel.config.js webpack.* +webpack jest.config.js samples \ No newline at end of file From 932de55917f9f3c1518be046c9df04300bc418e0 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Wed, 19 Aug 2026 20:07:31 +0530 Subject: [PATCH 091/103] SK-3041:Update flowvault-js sample. --- .../samples/using-typescript/Reveal-composable/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a22f3f2d..680862ee 100644 --- 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 @@ -10,7 +10,7 @@ import Skyflow, { RevealOptions, RevealResponse, SkyflowConfig, - ComposableRevealElement + ComposableRevealElement, SkyflowError, } from 'skyflow-flowvault-js'; From 88d689569c29eb97fe9dcfbe22a2d51e1e27ce2d Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Thu, 20 Aug 2026 00:12:24 +0530 Subject: [PATCH 092/103] SK-3041:Update test cases for flowvault package. --- .../skyflow-flowvault-js/jest.config.json | 11 +- .../tests/api-utils/collect.flowdb.test.js | 140 +++++++++++ .../tests/api-utils/reveal.flowdb.test.js | 167 +++++++++++++ .../collect/collect-container.flowdb.test.ts | 221 ++++++++++++++++++ .../collect/collect-variant.flowdb.test.ts | 48 ++++ .../composable-container.flowdb.test.ts | 16 ++ ...composable-reveal-container.flowdb.test.ts | 141 +++++++++++ .../reveal/reveal-container.flowdb.test.ts | 146 ++++++++++++ .../frame-element-init.flowdb.test.js | 72 ++++++ .../skyflow-frame-controller.flowdb.test.ts | 202 ++++++++++++++++ .../tests/libs/skyflow-flowdb-error.test.js | 9 + .../tests/re-exports.flowdb.test.ts | 75 ++++++ .../tests/utils/helpers.flowdb.test.ts | 24 ++ .../tests/utils/validators.flowdb.test.ts | 117 +++++++++- 14 files changed, 1387 insertions(+), 2 deletions(-) create mode 100644 packages/skyflow-flowvault-js/tests/core/external/collect/collect-container.flowdb.test.ts create mode 100644 packages/skyflow-flowvault-js/tests/core/external/collect/collect-variant.flowdb.test.ts create mode 100644 packages/skyflow-flowvault-js/tests/core/external/reveal/composable-reveal-container.flowdb.test.ts create mode 100644 packages/skyflow-flowvault-js/tests/core/external/reveal/reveal-container.flowdb.test.ts create mode 100644 packages/skyflow-flowvault-js/tests/core/internal/skyflow-frame-controller.flowdb.test.ts create mode 100644 packages/skyflow-flowvault-js/tests/re-exports.flowdb.test.ts diff --git a/packages/skyflow-flowvault-js/jest.config.json b/packages/skyflow-flowvault-js/jest.config.json index 7ef2f949..9a0dacd2 100644 --- a/packages/skyflow-flowvault-js/jest.config.json +++ b/packages/skyflow-flowvault-js/jest.config.json @@ -7,7 +7,16 @@ "!**/*.d.ts", "!/src/index.ts", "!/src/index-node.ts", - "!/src/index-internal.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, 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 index 95a212cc..41432ead 100644 --- a/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js +++ b/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js @@ -8,6 +8,7 @@ import { insertDataInCollectFlowDB, updateDataInCollectFlowDB, mergeFlowDBCollectResponses, + replaceCVVTokensInResponse, } from '../../src/api-utils/collect'; // Note: the flowvault collect data layer receives the auth token as a parameter @@ -24,6 +25,12 @@ describe('constructFlowDBInsertRequest', () => { ], }; + 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({ @@ -192,6 +199,12 @@ describe('constructFlowDBUpdateRequest', () => { ], }; + 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({ @@ -222,6 +235,16 @@ describe('constructFlowDBUpdateRequest', () => { }); 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: { @@ -258,6 +281,40 @@ describe('additionalFields (AdditionalFields) → flowDB request bodies', () => }); }); + 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('mixes inserts and skyflowId updates in a single additionalFields batch', () => { const options = { additionalFields: { @@ -452,3 +509,86 @@ describe('mergeFlowDBCollectResponses (mixed insert/update outcomes)', () => { 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 index ac3ef21b..37849f5f 100644 --- a/packages/skyflow-flowvault-js/tests/api-utils/reveal.flowdb.test.js +++ b/packages/skyflow-flowvault-js/tests/api-utils/reveal.flowdb.test.js @@ -12,6 +12,7 @@ import { } 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). @@ -19,6 +20,11 @@ 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', @@ -175,6 +181,18 @@ describe('constructFlowDBDetokenizeError', () => { 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', () => { @@ -222,6 +240,38 @@ describe('formatRecordsForClientFlowDB', () => { }, }); }); + + 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', () => { @@ -265,6 +315,29 @@ describe('formatRecordsForClientComposableFlowDB', () => { 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', () => { @@ -363,6 +436,55 @@ describe('fetchRecordsByTokenIdFlowDB', () => { }, }); }); + + 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', () => { @@ -445,4 +567,49 @@ describe('fetchRecordsByTokenIdComposableFlowDB', () => { }, }); }); + + 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 00000000..c4c581fa --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/collect/collect-container.flowdb.test.ts @@ -0,0 +1,221 @@ +/* + 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 { ElementType } 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: ElementType.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: ElementType.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); + }); + }); + + // 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 00000000..7e4e58ac --- /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 index dbb102f2..a972febb 100644 --- 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 @@ -286,4 +286,20 @@ describe('flowDB composable collect container', () => { .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 00000000..369950b0 --- /dev/null +++ b/packages/skyflow-flowvault-js/tests/core/external/reveal/composable-reveal-container.flowdb.test.ts @@ -0,0 +1,141 @@ +/* + 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 { 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); + }); + + // 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 00000000..25a1b39b --- /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/frame-element-init.flowdb.test.js b/packages/skyflow-flowvault-js/tests/core/internal/frame-element-init.flowdb.test.js index 31d125d8..00a5f699 100644 --- 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 @@ -297,3 +297,75 @@ describe('FrameElementInit tokenize (flowDB variant)', () => { 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 00000000..7fa7cd6c --- /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/libs/skyflow-flowdb-error.test.js b/packages/skyflow-flowvault-js/tests/libs/skyflow-flowdb-error.test.js index e7d68238..36f20dd4 100644 --- a/packages/skyflow-flowvault-js/tests/libs/skyflow-flowdb-error.test.js +++ b/packages/skyflow-flowvault-js/tests/libs/skyflow-flowdb-error.test.js @@ -43,6 +43,15 @@ describe('normalizeFlowDBError', () => { 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' }); 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 00000000..d7fb287e --- /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/utils/helpers.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts index 7fea4129..487da32d 100644 --- a/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts +++ b/packages/skyflow-flowvault-js/tests/utils/helpers.flowdb.test.ts @@ -9,8 +9,13 @@ 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); @@ -33,3 +38,22 @@ describe('generateMockCVV', () => { 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/validators.flowdb.test.ts b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts index 16c535e6..c8bd885e 100644 --- a/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts +++ b/packages/skyflow-flowvault-js/tests/utils/validators.flowdb.test.ts @@ -14,8 +14,11 @@ import { validateFlowDBUpsertOptions, validateFlowDBAdditionalFieldsInCollect, validateCollectElementOptions, + validateRevealElementRecords, + validateRevealOptions, + validateCollectElementInput, } from '../../src/utils/validators'; -import { UpdateType } from '../../src/utils/common'; +import { UpdateType, LogLevel } from '../../src/utils/common'; describe('validateFlowDBUpsertOptions', () => { test('B1: accepts a valid flowDB upsert ({ tableName, uniqueColumns })', () => { @@ -64,6 +67,12 @@ describe('validateFlowDBUpsertOptions', () => { ])).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/); @@ -144,3 +153,109 @@ describe('validateCollectElementOptions', () => { .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(); + }); +}); From b0692a127df585f97c25817d6f418fa5a94c6461 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Thu, 20 Aug 2026 16:11:52 +0530 Subject: [PATCH 093/103] SK-3041:Update loadash package. --- package-lock.json | 21 +++++++++++---------- packages/skyflow-flowvault-js/package.json | 2 +- packages/skyflow-js/package.json | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1121415b..619df5e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9421,9 +9421,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://prekarilabs.jfrog.io/prekarilabs/api/npm/npm/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -13445,7 +13446,7 @@ "jss": "10.10.0", "jss-preset-default": "10.10.0", "jwt-decode": "3.1.2", - "lodash": "4.17.21", + "lodash": "4.18.1", "mime": "3.0.0", "regex-parser": "2.3.1", "set-value": "4.1.0" @@ -13467,7 +13468,7 @@ "jss": "10.10.0", "jss-preset-default": "10.10.0", "jwt-decode": "3.1.2", - "lodash": "4.17.21", + "lodash": "4.18.1", "mime": "3.0.0", "regex-parser": "2.3.1", "set-value": "4.1.0" @@ -20532,9 +20533,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://prekarilabs.jfrog.io/prekarilabs/api/npm/npm/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "lodash.debounce": { "version": "4.0.8", @@ -22211,7 +22212,7 @@ "jss": "10.10.0", "jss-preset-default": "10.10.0", "jwt-decode": "3.1.2", - "lodash": "4.17.21", + "lodash": "4.18.1", "mime": "3.0.0", "regex-parser": "2.3.1", "set-value": "4.1.0" @@ -22228,7 +22229,7 @@ "jss": "10.10.0", "jss-preset-default": "10.10.0", "jwt-decode": "3.1.2", - "lodash": "4.17.21", + "lodash": "4.18.1", "mime": "3.0.0", "regex-parser": "2.3.1", "set-value": "4.1.0" diff --git a/packages/skyflow-flowvault-js/package.json b/packages/skyflow-flowvault-js/package.json index 34869b8d..2d08008b 100644 --- a/packages/skyflow-flowvault-js/package.json +++ b/packages/skyflow-flowvault-js/package.json @@ -41,7 +41,7 @@ "jss": "10.10.0", "jss-preset-default": "10.10.0", "jwt-decode": "3.1.2", - "lodash": "4.17.21", + "lodash": "4.18.1", "mime": "3.0.0", "regex-parser": "2.3.1", "set-value": "4.1.0" diff --git a/packages/skyflow-js/package.json b/packages/skyflow-js/package.json index 6ea314d3..050f5699 100644 --- a/packages/skyflow-js/package.json +++ b/packages/skyflow-js/package.json @@ -41,7 +41,7 @@ "jss": "10.10.0", "jss-preset-default": "10.10.0", "jwt-decode": "3.1.2", - "lodash": "4.17.21", + "lodash": "4.18.1", "mime": "3.0.0", "regex-parser": "2.3.1", "set-value": "4.1.0" From 4f660492b7b6599a225388f19b86e866d92df052 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Thu, 20 Aug 2026 17:09:00 +0530 Subject: [PATCH 094/103] SK-3041:Fix prototype pollution. --- core/api-utils/collect.ts | 6 +- core/utils/safe-merge/index.ts | 39 +++++++++ .../src/api-utils/collect.ts | 6 +- .../tests/api-utils/collect.flowdb.test.js | 20 +++++ .../tests/api-utils/collect.test.ts | 18 ++++ .../skyflow-js/tests/utils/safe-merge.test.ts | 83 +++++++++++++++++++ 6 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 core/utils/safe-merge/index.ts create mode 100644 packages/skyflow-js/tests/utils/safe-merge.test.ts diff --git a/core/api-utils/collect.ts b/core/api-utils/collect.ts index 53fe6aae..e730b791 100644 --- a/core/api-utils/collect.ts +++ b/core/api-utils/collect.ts @@ -7,8 +7,8 @@ Copyright (c) 2022 Skyflow, Inc. // 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 merge from 'lodash/merge'; 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'; @@ -48,7 +48,7 @@ export const constructElementsInsertReq = (req, update, options) => { record.fields, update[record.fields.skyflowID], record.table, ); const temp = record.fields; - merge(temp, update[record.fields.skyflowID]); + safeMerge(temp, update[record.fields.skyflowID]); update[record.fields.skyflowID] = temp; } else { update[record.fields.skyflowID] = { @@ -60,7 +60,7 @@ export const constructElementsInsertReq = (req, update, options) => { if (tables.includes(record.table)) { checkDuplicateColumns(record.fields, req[record.table], record.table); const temp = record.fields; - merge(temp, req[record.table]); + safeMerge(temp, req[record.table]); req[record.table] = temp; } else { req[record.table] = record.fields; diff --git a/core/utils/safe-merge/index.ts b/core/utils/safe-merge/index.ts new file mode 100644 index 00000000..29b26244 --- /dev/null +++ b/core/utils/safe-merge/index.ts @@ -0,0 +1,39 @@ +/* +Copyright (c) 2024 Skyflow, Inc. +*/ +// Prototype-pollution-safe wrapper around lodash/merge. The collect/update +// request assembly deep-merges caller-influenced objects (additionalFields) with +// collected element data; a `__proto__` / `constructor` / `prototype` key in a +// source could otherwise reach Object.prototype during the recursive merge. +// We strip those keys from every source, then delegate to lodash/merge so the +// merge semantics (deep merge, in-place mutation of `target`, array handling) +// stay identical to the previous direct `merge(target, source)` calls for all +// legitimate inputs. `target` is mutated and returned exactly as lodash does. +import merge from 'lodash/merge'; + +const FORBIDDEN_KEYS = new Set(['__proto__', 'prototype', 'constructor']); + +// Returns a copy of `value` with dangerous keys removed at every depth. Arrays, +// nested objects and primitive values are otherwise preserved as-is so the +// downstream merge produces the same result it did before. +const sanitize = (value: any): any => { + if (Array.isArray(value)) { + return value.map((item) => sanitize(item)); + } + if (value !== null && typeof value === 'object') { + const result: Record = {}; + Object.keys(value).forEach((key) => { + if (FORBIDDEN_KEYS.has(key)) return; + result[key] = sanitize(value[key]); + }); + return result; + } + return value; +}; + +export const safeMerge = (target: T, ...sources: any[]): T => merge( + target, + ...sources.map((source) => sanitize(source)), +); + +export default safeMerge; diff --git a/packages/skyflow-flowvault-js/src/api-utils/collect.ts b/packages/skyflow-flowvault-js/src/api-utils/collect.ts index c4a9d48c..35564f77 100644 --- a/packages/skyflow-flowvault-js/src/api-utils/collect.ts +++ b/packages/skyflow-flowvault-js/src/api-utils/collect.ts @@ -5,8 +5,8 @@ Copyright (c) 2025 Skyflow, Inc. // 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 merge from 'lodash/merge'; import omit from 'lodash/omit'; +import { safeMerge } from '@core/utils/safe-merge'; import { IInsertRecordInput, IInsertRecord, CVVMap, } from '@core/types'; @@ -45,7 +45,7 @@ export const constructElementsInsertReq = (req, update, options) => { if (ids.includes(skyflowId)) { checkDuplicateColumns(data, update[skyflowId], tableName); const temp = { ...data }; - merge(temp, update[skyflowId]); + safeMerge(temp, update[skyflowId]); update[skyflowId] = temp; } else { update[skyflowId] = { @@ -56,7 +56,7 @@ export const constructElementsInsertReq = (req, update, options) => { } else if (tables.includes(tableName)) { checkDuplicateColumns(data, req[tableName], tableName); const temp = { ...data }; - merge(temp, req[tableName]); + safeMerge(temp, req[tableName]); req[tableName] = temp; } else { req[tableName] = { ...data }; 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 index 41432ead..d8b5be7f 100644 --- a/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js +++ b/packages/skyflow-flowvault-js/tests/api-utils/collect.flowdb.test.js @@ -315,6 +315,26 @@ describe('additionalFields (AdditionalFields) → flowDB request bodies', () => ]); }); + 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: { diff --git a/packages/skyflow-js/tests/api-utils/collect.test.ts b/packages/skyflow-js/tests/api-utils/collect.test.ts index 130e5630..f504b026 100644 --- a/packages/skyflow-js/tests/api-utils/collect.test.ts +++ b/packages/skyflow-js/tests/api-utils/collect.test.ts @@ -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/packages/skyflow-js/tests/utils/safe-merge.test.ts b/packages/skyflow-js/tests/utils/safe-merge.test.ts new file mode 100644 index 00000000..f17547f0 --- /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(); + }); +}); From 8c7a3a8987279b7f28a1631458ed30602897ab60 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Fri, 21 Aug 2026 00:20:17 +0530 Subject: [PATCH 095/103] SK-3041:update url validator. --- core/validators/index.ts | 10 +++++----- packages/skyflow-js/tests/utils/helpers.test.js | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/core/validators/index.ts b/core/validators/index.ts index 2674f007..2626e628 100644 --- a/core/validators/index.ts +++ b/core/validators/index.ts @@ -101,17 +101,17 @@ export const isValidExpiryYearFormat = (format: string): boolean => { }; export const isValidURL = (url: string) => { - if (!url || url.substring(0, 5).toLowerCase() !== 'https') { + if (!url) { return false; } try { - const tempUrl = new URL(url); - if (tempUrl) return true; + // Gate on the parsed scheme rather than a substring prefix so that + // near-miss schemes like `httpsx://` (which start with "https" but are + // not TLS) are rejected. `new URL` also rejects malformed URLs. + return new URL(url).protocol === 'https:'; } catch (err) { return false; } - - return true; }; export const isValidRegExp = (input) => { diff --git a/packages/skyflow-js/tests/utils/helpers.test.js b/packages/skyflow-js/tests/utils/helpers.test.js index 62cae25e..0a975725 100644 --- a/packages/skyflow-js/tests/utils/helpers.test.js +++ b/packages/skyflow-js/tests/utils/helpers.test.js @@ -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); + }); }); From ba39ed6babf50ebd882a53734d91a4f32c1a3f1d Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Fri, 21 Aug 2026 18:50:45 +0530 Subject: [PATCH 096/103] SK-3041:Address feedback items source code changes. --- core/constants.ts | 97 +++++++++++-------- core/external/base-skyflow.ts | 8 +- core/external/collect/collect-container.ts | 6 +- core/external/collect/collect-element.ts | 9 +- .../collect/composable-collect-container.ts | 13 ++- .../collect/composable-collect-element.ts | 16 ++- core/external/common/composable-container.ts | 10 +- .../reveal/composable-reveal-container.ts | 5 + core/helpers/index.ts | 4 +- core/internal/iframe-form/index.ts | 21 ++-- core/internal/index.ts | 12 +-- core/libs/element-options.ts | 4 +- core/types/index.ts | 6 +- .../skyflow-flowvault-js/src/index-node.ts | 6 +- .../src/internal/internal-types/index.ts | 4 +- packages/skyflow-flowvault-js/src/skyflow.ts | 8 +- .../src/utils/common/index.ts | 16 ++- .../src/utils/validators/index.ts | 30 ++++++ .../collect/collect-container.flowdb.test.ts | 31 +++++- .../composable-container.flowdb.test.ts | 53 +++++++++- ...composable-reveal-container.flowdb.test.ts | 10 ++ packages/skyflow-js/src/index-node.ts | 5 +- .../src/internal/internal-types/index.ts | 4 +- packages/skyflow-js/src/skyflow.ts | 7 ++ packages/skyflow-js/src/utils/common/index.ts | 12 ++- .../collect/collect-container.test.js | 2 +- .../collect/collect-container.test.ts | 10 +- .../external/collect/collect-element.test.js | 8 +- .../external/collect/collect-element.test.ts | 10 +- .../collect/composable-container.test.js | 4 +- .../collect/composable-container.test.ts | 6 +- .../collect/composable-element.test.ts | 12 +-- .../external/reveal/reveal-element.test.ts | 2 +- .../core/internal/frame-element-init.test.js | 6 +- .../internal/iframe-form/iframe-form.test.js | 34 +++---- .../core/internal/internal-index.test.js | 4 +- .../skyflow-frame-controller.test.js | 2 +- .../tests/libs/element-options.test.js | 82 ++++++++-------- packages/skyflow-js/tests/skyflow.test.js | 10 +- .../skyflow-js/tests/utils/helpers.test.js | 62 ++++++------ 40 files changed, 424 insertions(+), 227 deletions(-) diff --git a/core/constants.ts b/core/constants.ts index 9fadf350..aed457ed 100644 --- a/core/constants.ts +++ b/core/constants.ts @@ -168,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', @@ -177,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', @@ -254,7 +271,7 @@ export const ELEMENTS = { }, sensitive: false, }, - [ElementType.CARDHOLDER_NAME]: { + [BaseElementType.CARDHOLDER_NAME]: { name: 'cardHolderName', attributes: { type: 'text', @@ -263,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', @@ -274,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', @@ -285,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, @@ -296,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, @@ -306,7 +323,7 @@ export const ELEMENTS = { }, sensitive: true, }, - [ElementType.CVV]: { + [BaseElementType.CVV]: { name: 'CVV', attributes: { type: 'text', @@ -316,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', @@ -334,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: { @@ -647,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 = { @@ -689,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/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index de283486..f55302c6 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -37,7 +37,6 @@ import uuid from '@core/libs/uuid'; import isTokenValid from '@core/utils/jwt-utils'; import { CardType, - ElementType, ELEMENT_EVENTS_TO_IFRAME, SDK_VERSION_KEY, SESSION_ID, @@ -390,9 +389,10 @@ abstract class BaseSkyflow< return ContainerType; } - static get ElementType() { - return ElementType; - } + // `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; diff --git a/core/external/collect/collect-container.ts b/core/external/collect/collect-container.ts index 01fae4db..3c7b4387 100644 --- a/core/external/collect/collect-container.ts +++ b/core/external/collect/collect-container.ts @@ -13,7 +13,7 @@ import { CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, FRAME_ELEMENT, COLLECT_TYPES, - ElementType, + AnyElementType, } from '@core/constants'; import properties from '@core/properties'; import Container from '@core/external/common/container'; @@ -49,7 +49,7 @@ import { // package's own `ICollectElement extends ICollectElementBase` adds. `column` is // the shared column key. export interface ICollectElementBase { - elementType: ElementType; + elementType: AnyElementType; elementName: string; name: string; column?: string; @@ -62,7 +62,7 @@ export interface ICollectElementBase { } export interface ElementGroupItem extends CollectElementInput, ICollectElementOptionsBase { - elementType: ElementType; + elementType: AnyElementType; name?: string; accept?: string[]; elementName?: string; diff --git a/core/external/collect/collect-element.ts b/core/external/collect/collect-element.ts index 478abaf2..73e90b65 100644 --- a/core/external/collect/collect-element.ts +++ b/core/external/collect/collect-element.ts @@ -20,7 +20,8 @@ import { EVENT_TYPES, METRIC_TYPES, ELEMENT_TYPES, - ElementType, + BaseElementType, + FileElementType, } from '@core/constants'; import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import logs from '@core/utils/logs'; @@ -484,7 +485,7 @@ class CollectElement< 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); @@ -562,8 +563,8 @@ class CollectElement< 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; diff --git a/core/external/collect/composable-collect-container.ts b/core/external/collect/composable-collect-container.ts index edf4bfd3..ea22e3ea 100644 --- a/core/external/collect/composable-collect-container.ts +++ b/core/external/collect/composable-collect-container.ts @@ -244,10 +244,15 @@ abstract class CoreComposableCollectContainer< printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), MessageType.LOG, this.context.logLevel); - callback({ - client: this.metaData.clientJSON, - context: this.context, - }); + // 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; }); } diff --git a/core/external/collect/composable-collect-element.ts b/core/external/collect/composable-collect-element.ts index 80fe8d73..c7db54b9 100644 --- a/core/external/collect/composable-collect-element.ts +++ b/core/external/collect/composable-collect-element.ts @@ -10,7 +10,13 @@ Copyright (c) 2023 Skyflow, Inc. import { Context } from 'vm'; import EventEmitter from '@core/event-emitter'; import SKYFLOW_ERROR_CODE from '@core/utils/constants'; -import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '@core/constants'; +import { + 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'; @@ -43,7 +49,7 @@ class ComposableElement< #context: Context; - #elementType: ElementType; + #elementType: AnyElementType; constructor(name, eventEmitter, iframeName, metaData) { this.#elementName = name; @@ -58,7 +64,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) { @@ -89,7 +95,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; @@ -133,7 +139,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 index b777f6f9..7b65f656 100644 --- a/core/external/common/composable-container.ts +++ b/core/external/common/composable-container.ts @@ -91,6 +91,14 @@ abstract class ComposableContainerBase< 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: ( @@ -134,7 +142,7 @@ abstract class ComposableContainerBase< src: getIframeSrc(), }); setStyles(iframe, { ...CONTROLLER_STYLES }); - printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, this.getClassName()), + printLog(parameterizedString(this.getCreateContainerLog(), this.getClassName()), MessageType.LOG, this.context.logLevel); this.containerMounted = true; diff --git a/core/external/reveal/composable-reveal-container.ts b/core/external/reveal/composable-reveal-container.ts index d25739ec..3fdacb05 100644 --- a/core/external/reveal/composable-reveal-container.ts +++ b/core/external/reveal/composable-reveal-container.ts @@ -54,6 +54,11 @@ abstract class CoreComposableRevealContainer< 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 diff --git a/core/helpers/index.ts b/core/helpers/index.ts index 950fb6f8..a0308385 100644 --- a/core/helpers/index.ts +++ b/core/helpers/index.ts @@ -10,7 +10,7 @@ import { import SkyflowError from '@core/errors'; import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import { - ALLOWED_NAME_FOR_FILE, CardType, ElementType, COPY_UTILS, + ALLOWED_NAME_FOR_FILE, CardType, BaseElementType, COPY_UTILS, DEFAULT_INPUT_FORMAT_TRANSLATION, CORALOGIX_DOMAIN, } from '@core/constants'; import properties from '@core/properties'; @@ -123,7 +123,7 @@ export const getContainerType = (frameName:string):ContainerType => { 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); diff --git a/core/internal/iframe-form/index.ts b/core/internal/iframe-form/index.ts index 9312a928..ef8ffe9c 100644 --- a/core/internal/iframe-form/index.ts +++ b/core/internal/iframe-form/index.ts @@ -10,7 +10,8 @@ import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, - ElementType, + BaseElementType, + FileElementType, FRAME_ELEMENT, DEFAULT_ERROR_TEXT_ELEMENT_TYPES, DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES, @@ -164,7 +165,7 @@ export default class IFrameFormElement extends EventEmitter { if (inputElement) { let elementValue = inputElement?.value; - if (elementValue && this.fieldType === ElementType.CARD_NUMBER) { + if (elementValue && this.fieldType === BaseElementType.CARD_NUMBER) { elementValue = elementValue.replace(/[\s-]/g, ''); } @@ -191,7 +192,7 @@ export default class IFrameFormElement extends EventEmitter { bus.on(ELEMENT_EVENTS_TO_CLIENT.BLUR + iframeName, () => { let { value } = this.state; - if (value && this.fieldType === ElementType.CARD_NUMBER) { + if (value && this.fieldType === BaseElementType.CARD_NUMBER) { value = value.replace(/[\s-]/g, ''); } // Validate the match and update the state accordingly @@ -516,19 +517,19 @@ export default class IFrameFormElement extends EventEmitter { let vaildateFileNames = true; let fileSpecificError = ''; - if (this.fieldType === ElementType.CARD_NUMBER && value) { + if (this.fieldType === BaseElementType.CARD_NUMBER && value) { if (this.regex) { resp = this.regex.test(value) && validateCreditCardNumber(value) && validateCardNumberLengthCheck(value); } - } else if (this.fieldType === ElementType.EXPIRATION_DATE && value) { + } else if (this.fieldType === BaseElementType.EXPIRATION_DATE && value) { resp = validateExpiryDate(value, this.format); - } else if (this.fieldType === ElementType.EXPIRATION_MONTH && value) { + } else if (this.fieldType === BaseElementType.EXPIRATION_MONTH && value) { resp = validateExpiryMonth(value); - } else if (this.fieldType === ElementType.EXPIRATION_YEAR && value) { + } else if (this.fieldType === BaseElementType.EXPIRATION_YEAR && value) { resp = validateExpiryYear(value, this.format); - } else if (this.fieldType === ElementType.FILE_INPUT) { + } else if (this.fieldType === FileElementType.FILE_INPUT) { try { resp = fileValidation(value, this.state.isRequired, { allowedFileType: this.allowedFileType, @@ -538,7 +539,7 @@ export default class IFrameFormElement extends EventEmitter { resp = false; } if (this.preserveFileName) vaildateFileNames = vaildateFileName(value.name); - } else if (this.fieldType === ElementType.MULTI_FILE_INPUT) { + } else if (this.fieldType === FileElementType.MULTI_FILE_INPUT) { const files = this.state.value instanceof FileList ? Array.from(this.state.value) : [this.state.value]; @@ -636,7 +637,7 @@ export default class IFrameFormElement extends EventEmitter { let resp = true; if (this.validations && this.validations.length) { for (let i = 0; i < this.validations.length; i += 1) { - if (this.fieldType === ElementType.CARD_NUMBER) { + if (this.fieldType === BaseElementType.CARD_NUMBER) { value = value.replace(/[\s-]/g, ''); } switch (this.validations[i].type) { diff --git a/core/internal/index.ts b/core/internal/index.ts index a0e8b6fd..c54c4604 100644 --- a/core/internal/index.ts +++ b/core/internal/index.ts @@ -20,7 +20,7 @@ import { COLLECT_ELEMENT_LABEL_DEFAULT_STYLES, CARD_ENCODED_ICONS, INPUT_WITH_ICON_STYLES, - ElementType, + BaseElementType, INPUT_WITH_ICON_DEFAULT_STYLES, CARD_NUMBER_MASK, EXPIRY_DATE_MASK, @@ -332,7 +332,7 @@ export default class FrameElement { if (this.iFrameFormElement.containerType === ContainerType.COMPOSABLE) { const elementType = this.iFrameFormElement.fieldType; const fieldTypeCheck = ALLOWED_FOCUS_AUTO_SHIFT_ELEMENT_TYPES - .includes(elementType as ElementType); + .includes(elementType as BaseElementType); if (state.value && state.isComplete && state.isValid && fieldTypeCheck) { const inputElements = document.getElementsByTagName('input') as any; let elementList = []; @@ -1045,7 +1045,7 @@ export default class FrameElement { ...INPUT_STYLES, ...options.inputStyles.base, }; - if (options.elementType === ElementType.CARD_NUMBER && this.options.enableCardIcon) { + if (options.elementType === BaseElementType.CARD_NUMBER && this.options.enableCardIcon) { options.inputStyles.base = { ...INPUT_WITH_ICON_STYLES, ...options.inputStyles.base, @@ -1053,7 +1053,7 @@ export default class FrameElement { } this.injectInputStyles(options.inputStyles); - } else if (options.elementType === ElementType.CARD_NUMBER && this.options.enableCardIcon) { + } else if (options.elementType === BaseElementType.CARD_NUMBER && this.options.enableCardIcon) { this.injectInputStyles({ base: { ...INPUT_WITH_ICON_DEFAULT_STYLES } }); } if (Object.prototype.hasOwnProperty.call(options, 'label')) { @@ -1125,10 +1125,10 @@ export default class FrameElement { const id: any = this.domInput; this.iFrameFormElement.setValidation(this.options.validations); this.iFrameFormElement.setReplacePattern(this.options.replacePattern); - if (options.elementType === ElementType.EXPIRATION_DATE) { + if (options.elementType === BaseElementType.EXPIRATION_DATE) { this.iFrameFormElement.setFormat(options.format); this.iFrameFormElement.setMask(EXPIRY_DATE_MASK[options.format] as string[]); - } else if (options.elementType === ElementType.EXPIRATION_YEAR) { + } else if (options.elementType === BaseElementType.EXPIRATION_YEAR) { this.iFrameFormElement.setFormat(options.format); this.iFrameFormElement.setMask(EXPIRY_YEAR_MASK[options.format] as string[]); } else { diff --git a/core/libs/element-options.ts b/core/libs/element-options.ts index 2ff35d06..4fdcd9e1 100644 --- a/core/libs/element-options.ts +++ b/core/libs/element-options.ts @@ -11,7 +11,7 @@ import { DEFAULT_EXPIRATION_YEAR_FORMAT, DEFAULT_INPUT_FORMAT_TRANSLATION, ELEMENTS, - ElementType, + AnyElementType, INPUT_FORMATTING_NOT_SUPPORTED_ELEMENT_TYPES, INPUT_STYLES, } from '@core/constants'; @@ -270,7 +270,7 @@ IValidationRule[] | undefined => { }; export const formatOptions = ( - elementType: ElementType, + elementType: AnyElementType, options: ICollectElementOptionsBase & IFileCollectElementOptions, logLevel: LogLevel, ) => { diff --git a/core/types/index.ts b/core/types/index.ts index 6d02b845..fc00a02a 100644 --- a/core/types/index.ts +++ b/core/types/index.ts @@ -1,7 +1,7 @@ /* Copyright (c) 2025 Skyflow, Inc. */ -import { CardType, ElementType } from '@core/constants'; +import { CardType, AnyElementType } from '@core/constants'; import EventEmitter from '@core/event-emitter'; declare global { @@ -376,7 +376,7 @@ export interface ICollectElementUpdateOptionsBase extends ICollectElementInputBa // own public CollectElementInput (base + type + its own identity keys) that is // structurally assignable to this. export interface CollectElementInput extends ICollectElementInputBase { - type: ElementType, + type: AnyElementType, } // ---- Behavioral contracts -------------------------------------------------- @@ -613,7 +613,7 @@ export interface InternalState { isFocused: boolean, isRequired: boolean, name: string; - elementType: ElementType; + elementType: AnyElementType; isComplete: boolean; value: string | Blob | undefined; selectedCardScheme: string; diff --git a/packages/skyflow-flowvault-js/src/index-node.ts b/packages/skyflow-flowvault-js/src/index-node.ts index 6aa13648..9251add0 100644 --- a/packages/skyflow-flowvault-js/src/index-node.ts +++ b/packages/skyflow-flowvault-js/src/index-node.ts @@ -28,8 +28,9 @@ export { 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, - RequestMethod, + ElementType, ValidationRuleType, IValidationRule as ValidationRule, EventName, @@ -55,9 +56,10 @@ export { 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, - ElementType, } from '@core/constants'; export { diff --git a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts index b154b63a..744f63f5 100644 --- a/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts +++ b/packages/skyflow-flowvault-js/src/internal/internal-types/index.ts @@ -10,7 +10,7 @@ import { RedactionType, ContainerType, ClientMetadata, ElementInfo, ICollectResponseBase, IRevealResponseBase, } from '@core/types'; -import { ElementType } from '@core/constants'; +import { BaseElementType } from '@core/constants'; import { UpdateType, ICollectOptions } from '../../utils/common'; // COLLECT tokenize input consumed by the skyflow-frame-controller. Mirrors the @@ -39,7 +39,7 @@ export type { // (flowvault does not import the privacyDB container classes). export interface SkyflowElementProps { id: string; - type: ElementType; + type: BaseElementType; element: HTMLElement; container: any; } diff --git a/packages/skyflow-flowvault-js/src/skyflow.ts b/packages/skyflow-flowvault-js/src/skyflow.ts index 9ff43bdf..95f0e695 100644 --- a/packages/skyflow-flowvault-js/src/skyflow.ts +++ b/packages/skyflow-flowvault-js/src/skyflow.ts @@ -31,7 +31,7 @@ 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 } from './utils/common'; +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. @@ -99,5 +99,11 @@ ComposableRevealContainer 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 index ccae4e6d..2fae7982 100644 --- a/packages/skyflow-flowvault-js/src/utils/common/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/common/index.ts @@ -10,11 +10,16 @@ Copyright (c) 2025 Skyflow, Inc. // 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'; + // --- Reused, variant-neutral @core types (do not redefine) --- +// NOTE: RequestMethod is intentionally NOT re-exported — flowDB is elements-only +// (no invokeConnection / invokeGateway), so it would expose an unsupported surface. export { ErrorType, RedactionType, - RequestMethod, EventName, LogLevel, Env, @@ -22,6 +27,15 @@ export { 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, diff --git a/packages/skyflow-flowvault-js/src/utils/validators/index.ts b/packages/skyflow-flowvault-js/src/utils/validators/index.ts index b47a3aa9..14f34b44 100644 --- a/packages/skyflow-flowvault-js/src/utils/validators/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/validators/index.ts @@ -6,6 +6,7 @@ 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, @@ -102,6 +103,21 @@ export const validateRevealOptions = (options?: { tokenGroupRedactions?: any }) }); }; +// 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) => { @@ -117,6 +133,20 @@ export const validateCollectElementInput = (input: CollectElementInput, 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 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 index c4c581fa..fb67ff6e 100644 --- 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 @@ -8,7 +8,7 @@ // 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 { ElementType } from '@core/constants'; +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'; @@ -95,7 +95,7 @@ const cvvInput: CollectElementInput = { column: 'primary_card.cvv', placeholder: 'cvv', label: 'cvv', - type: ElementType.CVV, + type: BaseElementType.CVV, validations: [ { type: ValidationRuleType.LENGTH_MATCH_RULE, @@ -151,7 +151,7 @@ describe('flowDB collect container', () => { it('validateCreateInput: throws when skyflowId is not a string', () => { const container = new CollectContainer(metaData, context); expect(() => container.create({ - tableName: 'cards', column: 'cvv', type: ElementType.CVV, skyflowId: 123, + tableName: 'cards', column: 'cvv', type: BaseElementType.CVV, skyflowId: 123, } as any)).toThrow(SkyflowError); }); @@ -160,6 +160,31 @@ describe('flowDB collect container', () => { 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 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 index a972febb..0f6c42fa 100644 --- 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 @@ -7,10 +7,11 @@ // failure is wrapped as SkyflowFlowDBError, and the factory returns the container. import { ELEMENT_EVENTS_TO_IFRAME, - ElementType, + 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'; @@ -122,7 +123,7 @@ const cvvElementInput: CollectElementInput = { column: 'primary_card.cvv', placeholder: 'cvv', label: 'cvv', - type: ElementType.CVV, + type: BaseElementType.CVV, validations: [ { type: ValidationRuleType.LENGTH_MATCH_RULE, @@ -135,7 +136,7 @@ const cvvElementInput: CollectElementInput = { const cardNumberElement: CollectElementInput = { tableName: 'pii_fields', column: 'primary_card.card_number', - type: ElementType.CARD_NUMBER, + type: BaseElementType.CARD_NUMBER, ...collectStylesOptions, } as any; @@ -172,12 +173,58 @@ describe('flowDB composable collect container', () => { 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) => { 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 index 369950b0..27bb7db6 100644 --- 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 @@ -9,6 +9,7 @@ // 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'; @@ -65,6 +66,15 @@ describe('flowDB composable reveal container', () => { 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', () => { diff --git a/packages/skyflow-js/src/index-node.ts b/packages/skyflow-js/src/index-node.ts index 4d38ce02..70b34264 100644 --- a/packages/skyflow-js/src/index-node.ts +++ b/packages/skyflow-js/src/index-node.ts @@ -64,9 +64,12 @@ export type { ThreeDSBrowserDetails } from './external/threeds/threeds'; export { CardType, - ElementType, } 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'; diff --git a/packages/skyflow-js/src/internal/internal-types/index.ts b/packages/skyflow-js/src/internal/internal-types/index.ts index c8faf1be..a8499b81 100644 --- a/packages/skyflow-js/src/internal/internal-types/index.ts +++ b/packages/skyflow-js/src/internal/internal-types/index.ts @@ -1,5 +1,5 @@ import { ContainerType, ElementInfo, ClientMetadata } from '@core/types'; -import { ElementType } from '@core/constants'; +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'; @@ -35,7 +35,7 @@ export interface UploadFileDataInput extends ICollectOptions { export interface SkyflowElementProps { id: string; - type: ElementType; + type: AnyElementType; element: HTMLElement; container: CollectContainer | RevealContainer | ComposableContainer; } diff --git a/packages/skyflow-js/src/skyflow.ts b/packages/skyflow-js/src/skyflow.ts index e9ea584c..84a83f45 100644 --- a/packages/skyflow-js/src/skyflow.ts +++ b/packages/skyflow-js/src/skyflow.ts @@ -43,6 +43,7 @@ import { IUpdateRequest, UpdateResponse, IUpdateOptions, + ElementType, } from './utils/common'; import ComposableContainer from './external/collect/compose-collect-container'; import ThreeDS from './external/threeds/threeds'; @@ -157,5 +158,11 @@ ComposableRevealContainer static get ThreeDS() { return ThreeDS; } + + // 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 index 5428d7e7..2eee2b3f 100644 --- a/packages/skyflow-js/src/utils/common/index.ts +++ b/packages/skyflow-js/src/utils/common/index.ts @@ -10,11 +10,21 @@ import { ICollectOptionsBase, ICollectElementInputBase, ICollectElementOptionsBase, ICollectElementUpdateOptionsBase, IElementStateBase, IInsertRecordInput, } from '@core/types'; -import { ElementType } from '@core/constants'; +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). +// The runtime value and the type share one name (legal in TS). BaseElementType / +// FileElementType stay internal to @core and are never re-exported publicly. +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 { diff --git a/packages/skyflow-js/tests/core/external/collect/collect-container.test.js b/packages/skyflow-js/tests/core/external/collect/collect-container.test.js index d7e692cf..b1fbced4 100644 --- a/packages/skyflow-js/tests/core/external/collect/collect-container.test.js +++ b/packages/skyflow-js/tests/core/external/collect/collect-container.test.js @@ -3,7 +3,7 @@ Copyright (c) 2022 Skyflow, Inc. */ import { COLLECT_FRAME_CONTROLLER, - ElementType, + BaseElementType, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_EVENTS_TO_CONTAINER, SKYFLOW_FRAME_CONTROLLER_READY, diff --git a/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts b/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts index 34d95ce0..b0b83306 100644 --- a/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/collect-container.test.ts @@ -3,7 +3,7 @@ Copyright (c) 2025 Skyflow, Inc. */ import { ELEMENT_EVENTS_TO_IFRAME, - ElementType, + BaseElementType, FileElementType, } from "@core/constants"; import CollectContainer from "../../../../src/external/collect/collect-container"; import CollectElement from "@core/external/collect/collect-element"; @@ -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", }; diff --git a/packages/skyflow-js/tests/core/external/collect/collect-element.test.js b/packages/skyflow-js/tests/core/external/collect/collect-element.test.js index 867cc53c..83859fa2 100644 --- a/packages/skyflow-js/tests/core/external/collect/collect-element.test.js +++ b/packages/skyflow-js/tests/core/external/collect/collect-element.test.js @@ -5,7 +5,7 @@ import bus from 'framebus'; 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 '@core/constants'; +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'; @@ -693,7 +693,7 @@ const row = { }; describe('collect element validations', () => { - it('Invalid ElementType', () => { + it('Invalid BaseElementType', () => { const invalidElementType = [ { elements: [ @@ -1190,7 +1190,7 @@ 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() }; @@ -1243,7 +1243,7 @@ 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() }; diff --git a/packages/skyflow-js/tests/core/external/collect/collect-element.test.ts b/packages/skyflow-js/tests/core/external/collect/collect-element.test.ts index b8264301..efffde3c 100644 --- a/packages/skyflow-js/tests/core/external/collect/collect-element.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/collect-element.test.ts @@ -16,7 +16,7 @@ import { import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, - ElementType, + BaseElementType, } from "@core/constants"; import SKYFLOW_ERROR_CODE from "@core/utils/constants"; import { ContainerType } from "../../../../src/skyflow"; @@ -44,7 +44,7 @@ const input: CollectElementInput = { }, placeholder: "cvv", label: "cvv", - type: ElementType.CVV, + type: BaseElementType.CVV, }; const composableElementName = @@ -60,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 = { @@ -116,7 +116,7 @@ const composableRows = [ ]; const updateElementInput = { - elementType: ElementType.CVV, + elementType: BaseElementType.CVV, name: input.column, ...input, }; @@ -885,7 +885,7 @@ const row = { }; describe("testing collect element validations", () => { - it("Invalid ElementType", () => { + it("Invalid BaseElementType", () => { const invalidElementType = [ { elements: [ diff --git a/packages/skyflow-js/tests/core/external/collect/composable-container.test.js b/packages/skyflow-js/tests/core/external/collect/composable-container.test.js index 5c2df6d4..0f9c2324 100644 --- a/packages/skyflow-js/tests/core/external/collect/composable-container.test.js +++ b/packages/skyflow-js/tests/core/external/collect/composable-container.test.js @@ -2,7 +2,7 @@ import { COLLECT_FRAME_CONTROLLER, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_EVENTS_TO_CLIENT, - ElementType + BaseElementType, FileElementType } from '@core/constants'; import * as iframerUtils from '@core/iframe-libs/iframer'; import { LogLevel, Env, ValidationRuleType, ErrorType } from '../../../../src/utils/common'; @@ -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, } diff --git a/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts b/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts index a1747a68..71657c4b 100644 --- a/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/composable-container.test.ts @@ -3,7 +3,7 @@ */ import { ELEMENT_EVENTS_TO_IFRAME, - ElementType, + BaseElementType, } from "@core/constants"; import { LogLevel, @@ -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, }; diff --git a/packages/skyflow-js/tests/core/external/collect/composable-element.test.ts b/packages/skyflow-js/tests/core/external/collect/composable-element.test.ts index fadfe663..3c5b20d0 100644 --- a/packages/skyflow-js/tests/core/external/collect/composable-element.test.ts +++ b/packages/skyflow-js/tests/core/external/collect/composable-element.test.ts @@ -1,7 +1,7 @@ /* Copyright (c) 2025 Skyflow, Inc. */ -import { ELEMENT_EVENTS_TO_IFRAME, ElementType } from "@core/constants"; +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"; @@ -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/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts b/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts index f1cb31f7..b1a6f5c5 100644 --- a/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts +++ b/packages/skyflow-js/tests/core/external/reveal/reveal-element.test.ts @@ -8,7 +8,7 @@ import { ELEMENT_EVENTS_TO_CLIENT, REVEAL_TYPES, REVEAL_ELEMENT_OPTIONS_TYPES, - ElementType, + BaseElementType, CUSTOM_ERROR_MESSAGES, } from "@core/constants"; import RevealElement from "../../../../src/external/reveal/reveal-element"; diff --git a/packages/skyflow-js/tests/core/internal/frame-element-init.test.js b/packages/skyflow-js/tests/core/internal/frame-element-init.test.js index a50b0fb5..7835e330 100644 --- a/packages/skyflow-js/tests/core/internal/frame-element-init.test.js +++ b/packages/skyflow-js/tests/core/internal/frame-element-init.test.js @@ -1,5 +1,5 @@ import FrameElementInit from '../../../src/internal/frame-element-init'; -import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, ElementType, COLLECT_TYPES } from '@core/constants'; +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 '@core/errors'; import * as helpers from '../../../src/utils/helpers'; @@ -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/packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js b/packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js index e251b5f1..0d7adcf5 100644 --- a/packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js +++ b/packages/skyflow-js/tests/core/internal/iframe-form/iframe-form.test.js @@ -2,7 +2,7 @@ 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 '@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 '@core/internal/iframe-form' import * as busEvents from '@core/utils/bus-events'; @@ -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/packages/skyflow-js/tests/core/internal/internal-index.test.js b/packages/skyflow-js/tests/core/internal/internal-index.test.js index 50e1f561..484af58d 100644 --- a/packages/skyflow-js/tests/core/internal/internal-index.test.js +++ b/packages/skyflow-js/tests/core/internal/internal-index.test.js @@ -3,7 +3,7 @@ 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, ElementType, STYLE_TYPE } from '@core/constants'; +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/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js b/packages/skyflow-js/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.js index 85eb244a..030a5d48 100644 --- a/packages/skyflow-js/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,7 +2,7 @@ Copyright (c) 2022 Skyflow, Inc. */ import bus from 'framebus'; -import { COLLECT_TYPES, ELEMENT_EVENTS_TO_IFRAME, ELEMENT_TYPES, ElementType, PUREJS_TYPES, REVEAL_TYPES } from '@core/constants'; +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'; diff --git a/packages/skyflow-js/tests/libs/element-options.test.js b/packages/skyflow-js/tests/libs/element-options.test.js index f9850665..5e7c1e6d 100644 --- a/packages/skyflow-js/tests/libs/element-options.test.js +++ b/packages/skyflow-js/tests/libs/element-options.test.js @@ -1,4 +1,4 @@ -import { CARDNUMBER_INPUT_FORMAT, CardType, ElementType } from "@core/constants"; +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 "@core/utils/constants"; @@ -19,98 +19,98 @@ jest.mock('@core/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/packages/skyflow-js/tests/skyflow.test.js b/packages/skyflow-js/tests/skyflow.test.js index 8cc41e35..dbf466fa 100644 --- a/packages/skyflow-js/tests/skyflow.test.js +++ b/packages/skyflow-js/tests/skyflow.test.js @@ -6,7 +6,7 @@ import Skyflow, { ContainerType } from '../src/skyflow'; 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 { ElementType, ELEMENT_EVENTS_TO_IFRAME } from '@core/constants'; +import { BaseElementType, ELEMENT_EVENTS_TO_IFRAME } from '@core/constants'; import { Env, EventName, LogLevel, RedactionType, RequestMethod, ValidationRuleType } from '../src/utils/common'; import ComposableContainer from '../src/external/collect/compose-collect-container'; import SkyflowContainer from '../src/external/skyflow-container'; @@ -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/packages/skyflow-js/tests/utils/helpers.test.js b/packages/skyflow-js/tests/utils/helpers.test.js index 0a975725..b980fd69 100644 --- a/packages/skyflow-js/tests/utils/helpers.test.js +++ b/packages/skyflow-js/tests/utils/helpers.test.js @@ -1,7 +1,7 @@ /* Copyright (c) 2022 Skyflow, Inc. */ -import { CardType, ElementType,COPY_UTILS, CARD_NUMBER_MASK, DEFAULT_CARD_NUMBER_SEPERATOR, CARD_NUMBER_HYPEN_SEPERATOR } from '@core/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, @@ -47,48 +47,48 @@ jest.mock('@core/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") }) }) From ba24b9231013f451b0a91b74361e5f18daf9401f Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Fri, 21 Aug 2026 19:06:32 +0530 Subject: [PATCH 097/103] SK-3041:Address feedback items readme & sample changes. --- packages/skyflow-flowvault-js/README.md | 7 +++---- .../skyflow-elements-update-records/src/index.js | 8 ++++---- .../using-npm/skyflow-elements-update/package.json | 1 + .../using-script-tag/skyflow-elements-update-records.html | 8 ++++---- .../samples/using-script-tag/upsert-support.html | 1 + .../skyflow-elements-update-records/src/index.ts | 8 ++++---- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/skyflow-flowvault-js/README.md b/packages/skyflow-flowvault-js/README.md index a56ed207..8bad3da2 100644 --- a/packages/skyflow-flowvault-js/README.md +++ b/packages/skyflow-flowvault-js/README.md @@ -203,7 +203,6 @@ Everything below expands on each step: styling, validation, upsert, composable l - [**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) ## Using Skyflow Elements to collect data @@ -1620,8 +1619,8 @@ inputStyles: { } } ``` -The states that are available for `labelStyles` are `base`, `focus`, `global`. -* requiredAsterisk: styles applied for the Asterisk symbol in the label. +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: @@ -2910,7 +2909,7 @@ const cardNumberRevealElement = revealComposableContainer.create({ }); // Mount the reveal elements. -revealContainer.mount('#container'); // Assumes there is a div with container +revealComposableContainer.mount('#container'); // Assumes there is a div with container // ... // Update label, labelStyles properties on cardHolderNameRevealElement. 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 index e384775c..1f3c9bac 100644 --- 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 @@ -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({ @@ -115,7 +115,7 @@ try { { tableName: 'table1', data: { - skyflowId: '', + skyflowId: '', // Replace with a valid Skyflow ID of the record to update gender: 'MALE', }, }, 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 index cd4fd903..07aaeef5 100644 --- 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 @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "start": "parcel src/index.html --open", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], 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 index efd76811..b970b251 100644 --- 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 @@ -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({ @@ -152,7 +152,7 @@

    Collect Elements

    data: { gender: "MALE", }, - skyflowId: "", + skyflowId: "", // Replace with a valid Skyflow ID of the record to update }, { tableName: "table2", 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 index 72a1f0c4..b98010b6 100644 --- a/packages/skyflow-flowvault-js/samples/using-script-tag/upsert-support.html +++ b/packages/skyflow-flowvault-js/samples/using-script-tag/upsert-support.html @@ -143,6 +143,7 @@

    Reveal Elements

    { tableName: 'pii_fields', // table uniqueColumns: ['card_number'], // unique columns in the table + updateType: Skyflow.UpdateType.UPDATE, // optional, one of 'UPDATE' or 'REPLACE' }, ], }; 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 index 8ceb5fa5..008094f7 100644 --- 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 @@ -83,7 +83,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); @@ -95,7 +95,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); @@ -106,7 +106,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); @@ -134,7 +134,7 @@ try { data: { gender: 'MALE', }, - skyflowId: '', + skyflowId: '', // Replace with a valid Skyflow ID of the record to update }, { tableName: 'table2', From 9070929c1b3762abe59ca68a3f2ff2b739f900b2 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Mon, 24 Aug 2026 13:30:37 +0530 Subject: [PATCH 098/103] SK-3041:Address feedback items readme & sample changes. --- core/external/base-skyflow.ts | 8 ++++---- .../collect/composable-collect-container.ts | 5 ++++- .../collect/composable-collect-element.ts | 3 +-- packages/skyflow-flowvault-js/README.md | 15 ++++++++++++--- packages/skyflow-flowvault-js/samples/README.md | 2 +- packages/skyflow-flowvault-js/src/index-node.ts | 6 +++++- .../tests/skyflow.flowdb.test.js | 4 +++- .../skyflow-elements-update-records/src/index.js | 8 ++++---- .../skyflow-elements-update/package.json | 4 ++++ .../using-script-tag/composable-file-upload.html | 2 +- .../skyflow-elements-update-records.html | 8 ++++---- .../using-script-tag/skyflow-file-upload.html | 2 +- .../skyflow-elements-update-records/src/index.ts | 8 ++++---- packages/skyflow-js/src/skyflow.ts | 7 +++++++ 14 files changed, 55 insertions(+), 27 deletions(-) diff --git a/core/external/base-skyflow.ts b/core/external/base-skyflow.ts index f55302c6..b3343c30 100644 --- a/core/external/base-skyflow.ts +++ b/core/external/base-skyflow.ts @@ -76,7 +76,6 @@ import { LogLevel, MessageType, RedactionType, - RequestMethod, ValidationRuleType, } from '@core/types'; @@ -402,9 +401,10 @@ abstract class BaseSkyflow< return ErrorType; } - static get RequestMethod() { - return RequestMethod; - } + // 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; diff --git a/core/external/collect/composable-collect-container.ts b/core/external/collect/composable-collect-container.ts index ea22e3ea..ad150d31 100644 --- a/core/external/collect/composable-collect-container.ts +++ b/core/external/collect/composable-collect-container.ts @@ -103,8 +103,11 @@ abstract class CoreComposableCollectContainer< this.elementsList.push({ elementType: input.type, name: input.column, - ...input, + // 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, diff --git a/core/external/collect/composable-collect-element.ts b/core/external/collect/composable-collect-element.ts index c7db54b9..ac12f5f5 100644 --- a/core/external/collect/composable-collect-element.ts +++ b/core/external/collect/composable-collect-element.ts @@ -7,7 +7,6 @@ Copyright (c) 2023 Skyflow, Inc. // 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 { Context } from 'vm'; import EventEmitter from '@core/event-emitter'; import SKYFLOW_ERROR_CODE from '@core/utils/constants'; import { @@ -22,7 +21,7 @@ import properties from '@core/properties'; import SkyflowError from '@core/errors'; import { formatValidations } from '@core/libs/element-options'; import { - ICollectElementUpdateOptionsBase, EventName, MessageType, MetaData, ContainerType, + ICollectElementUpdateOptionsBase, EventName, MessageType, MetaData, ContainerType, Context, } from '@core/types'; import { printLog } from '@core/utils/logs-helper'; diff --git a/packages/skyflow-flowvault-js/README.md b/packages/skyflow-flowvault-js/README.md index 8bad3da2..776337dd 100644 --- a/packages/skyflow-flowvault-js/README.md +++ b/packages/skyflow-flowvault-js/README.md @@ -583,7 +583,7 @@ const container = skyflowClient.container(Skyflow.ContainerType.COLLECT); const element = container.create({ tableName: 'cards', column: 'cardNumber', - inputstyles: { + inputStyles: { base: { color: '#1d1d1d', }, @@ -1890,6 +1890,7 @@ state : { 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. @@ -2033,7 +2034,7 @@ const cvvElement = composableContainer.create({ }); // Mount the composable container. -composableContainer.mount('#compostableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. +composableContainer.mount('#composableContainer'); // Assumes there is a div with id='#composableContainer' in the webpage. // ... @@ -2198,6 +2199,8 @@ const options = { **Reveal Element Options examples:** Example 1 ```js +const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); + const revealElementInput = { token: '' }; @@ -2215,6 +2218,8 @@ Revealed Value displayed in element: "(123) 412-1234" Example 2: ```js +const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL); + const revealElementInput = { token: '' }; @@ -2408,7 +2413,7 @@ const revealButton = document.getElementById('revealPCIData'); if (revealButton) { revealButton.addEventListener('click', () => { - revealContainer.reveal().then((res) => { + container.reveal().then((res) => { //handle reveal response }).catch((err) => { cardNumber.setErrorOverride("custom error") @@ -2673,6 +2678,8 @@ const options = { **Reveal Element Options examples:** Example 1 ```js +const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + const revealElementInput = { token: '' }; @@ -2690,6 +2697,8 @@ Revealed Value displayed in element: "(123) 412-1234" Example 2: ```js +const revealComposableContainer = skyflowClient.container(Skyflow.ContainerType.COMPOSE_REVEAL, containerOptions); + const revealElementInput = { token: '' }; diff --git a/packages/skyflow-flowvault-js/samples/README.md b/packages/skyflow-flowvault-js/samples/README.md index 0e0bc67a..2973c638 100644 --- a/packages/skyflow-flowvault-js/samples/README.md +++ b/packages/skyflow-flowvault-js/samples/README.md @@ -116,7 +116,7 @@ Every sample exists in up to three flavors. Pick the one that matches how you co | masking | Masked input on Collect Elements | [html](using-script-tag/masking.html) | — | — | | card-brand-choice | Card brand choice (co-badged cards) | [html](using-script-tag/card-brand-choice.html) | — | — | | upsert-support | Upsert on insert (`tableName`/`uniqueColumns`/`updateType`) | [html](using-script-tag/upsert-support.html) | — | — | -| bearer-token-with-context | `getBearerTokenWithContext` token provider | [html](using-script-tag/bearer-token-with-context.html) | — | — | +| bearer-token-with-context | `getBearerToken` token provider with bound context (`.bind(...)`) | [html](using-script-tag/bearer-token-with-context.html) | — | — | --- diff --git a/packages/skyflow-flowvault-js/src/index-node.ts b/packages/skyflow-flowvault-js/src/index-node.ts index 9251add0..18a3ed97 100644 --- a/packages/skyflow-flowvault-js/src/index-node.ts +++ b/packages/skyflow-flowvault-js/src/index-node.ts @@ -79,8 +79,12 @@ 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 = CoreComposableElement; +export type ComposableElement = Omit, 'uploadMultipleFiles'>; export { default as CollectContainer } from './external/collect/collect-container'; export { default as ComposableContainer } from './external/collect/compose-collect-container'; diff --git a/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js b/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js index 7a41357f..bee2dad0 100644 --- a/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js +++ b/packages/skyflow-flowvault-js/tests/skyflow.flowdb.test.js @@ -59,7 +59,6 @@ describe('flowDB Skyflow (BaseSkyflow subclass wiring)', () => { expect(Skyflow.ElementType).toBeDefined(); expect(Skyflow.RedactionType).toBeDefined(); expect(Skyflow.ErrorType).toBeDefined(); - expect(Skyflow.RequestMethod).toBeDefined(); expect(Skyflow.LogLevel).toBeDefined(); expect(Skyflow.EventName).toBeDefined(); expect(Skyflow.Env).toBeDefined(); @@ -68,5 +67,8 @@ describe('flowDB Skyflow (BaseSkyflow subclass wiring)', () => { 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-js/samples/using-npm/skyflow-elements-update-records/src/index.js b/packages/skyflow-js/samples/using-npm/skyflow-elements-update-records/src/index.js index 7c4affa0..ef84f020 100644 --- a/packages/skyflow-js/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/packages/skyflow-js/samples/using-npm/skyflow-elements-update/package.json b/packages/skyflow-js/samples/using-npm/skyflow-elements-update/package.json index 0a55566e..fe22b2e4 100644 --- a/packages/skyflow-js/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-script-tag/composable-file-upload.html b/packages/skyflow-js/samples/using-script-tag/composable-file-upload.html index 4f852ef4..b49e50ae 100644 --- a/packages/skyflow-js/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/packages/skyflow-js/samples/using-script-tag/skyflow-elements-update-records.html b/packages/skyflow-js/samples/using-script-tag/skyflow-elements-update-records.html index 0dfa8a04..9e6933b3 100644 --- a/packages/skyflow-js/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/packages/skyflow-js/samples/using-script-tag/skyflow-file-upload.html b/packages/skyflow-js/samples/using-script-tag/skyflow-file-upload.html index 849303f8..98c91904 100644 --- a/packages/skyflow-js/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/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts b/packages/skyflow-js/samples/using-typescript/skyflow-elements-update-records/src/index.ts index 52af7456..e81e189d 100644 --- a/packages/skyflow-js/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/src/skyflow.ts b/packages/skyflow-js/src/skyflow.ts index 84a83f45..92e2c2ce 100644 --- a/packages/skyflow-js/src/skyflow.ts +++ b/packages/skyflow-js/src/skyflow.ts @@ -18,6 +18,7 @@ import { Context, ICoreMetadata, ISkyflow, + RequestMethod, SkyflowConfigOptions, } from '@core/types'; import logs from '@core/utils/logs'; @@ -159,6 +160,12 @@ ComposableRevealContainer 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() { From ae237c04d93fb47e14713d31c5065facd3ea1a52 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Mon, 24 Aug 2026 15:19:32 +0530 Subject: [PATCH 099/103] SK-3041:Address feedback items readme & sample changes. --- packages/skyflow-flowvault-js/src/utils/common/index.ts | 3 --- packages/skyflow-js/src/utils/common/index.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/skyflow-flowvault-js/src/utils/common/index.ts b/packages/skyflow-flowvault-js/src/utils/common/index.ts index 2fae7982..9aaf66b3 100644 --- a/packages/skyflow-flowvault-js/src/utils/common/index.ts +++ b/packages/skyflow-flowvault-js/src/utils/common/index.ts @@ -14,9 +14,6 @@ Copyright (c) 2025 Skyflow, Inc. // below. Not re-exported (only the derived ElementType is public). import { BaseElementType } from '@core/constants'; -// --- Reused, variant-neutral @core types (do not redefine) --- -// NOTE: RequestMethod is intentionally NOT re-exported — flowDB is elements-only -// (no invokeConnection / invokeGateway), so it would expose an unsupported surface. export { ErrorType, RedactionType, diff --git a/packages/skyflow-js/src/utils/common/index.ts b/packages/skyflow-js/src/utils/common/index.ts index 2eee2b3f..6d9c9d4b 100644 --- a/packages/skyflow-js/src/utils/common/index.ts +++ b/packages/skyflow-js/src/utils/common/index.ts @@ -19,8 +19,6 @@ export * from '@core/types'; // 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). -// The runtime value and the type share one name (legal in TS). BaseElementType / -// FileElementType stay internal to @core and are never re-exported publicly. export const ElementType = { ...BaseElementType, ...FileElementType }; // eslint-disable-next-line @typescript-eslint/no-redeclare export type ElementType = BaseElementType | FileElementType; From 901a4d29af3ac69a1e87ae4b3c5a6f59f9752fe8 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Mon, 24 Aug 2026 15:46:59 +0530 Subject: [PATCH 100/103] SK-3041:Update package-lock.json. --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 619df5e5..e75d575e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9422,7 +9422,7 @@ }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://prekarilabs.jfrog.io/prekarilabs/api/npm/npm/lodash/-/lodash-4.18.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, @@ -20534,7 +20534,7 @@ }, "lodash": { "version": "4.18.1", - "resolved": "https://prekarilabs.jfrog.io/prekarilabs/api/npm/npm/lodash/-/lodash-4.18.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "lodash.debounce": { From f3b282422ca2cd55c3a5f686f298ee6ddab10971 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Mon, 24 Aug 2026 18:30:37 +0530 Subject: [PATCH 101/103] SK-3041:Remove cardbran choice sample. --- .../using-script-tag/card-brand-choice.html | 244 ------------------ 1 file changed, 244 deletions(-) delete mode 100644 packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html diff --git a/packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html b/packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html deleted file mode 100644 index 94091520..00000000 --- a/packages/skyflow-flowvault-js/samples/using-script-tag/card-brand-choice.html +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - - Skyflow Elements - - - - - -

    Collect Elements

    - -
    -
    - -
    - -
    - -
    -
    
    -        
    -
    - - - - - - \ No newline at end of file From 53b63517f19dd381ab9d238c74967c1df2e7cb20 Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Tue, 25 Aug 2026 11:34:54 +0530 Subject: [PATCH 102/103] SK-3041:Remove docs folder. --- docs/package-split-plan.md | 302 --------------------------------- docs/phase-1-execution-plan.md | 201 ---------------------- docs/phase-2-execution-plan.md | 180 -------------------- docs/phase-3-execution-plan.md | 169 ------------------ 4 files changed, 852 deletions(-) delete mode 100644 docs/package-split-plan.md delete mode 100644 docs/phase-1-execution-plan.md delete mode 100644 docs/phase-2-execution-plan.md delete mode 100644 docs/phase-3-execution-plan.md diff --git a/docs/package-split-plan.md b/docs/package-split-plan.md deleted file mode 100644 index 01b59931..00000000 --- a/docs/package-split-plan.md +++ /dev/null @@ -1,302 +0,0 @@ -# Splitting `skyflow-js` into privacyDB + flowDB Packages — Architecture & Migration Plan - -**Status:** Proposed -**Scope:** Split the single `skyflow-js` codebase into two independently publishable npm packages that share one common core, with flowDB built by *extending* common interfaces. -**Sources:** privacyDB baseline restored from `main`; flowDB deltas taken from the `2.9.0-beta.1` tag. - ---- - -## 1. Goal - -Produce two independently versioned, independently published npm packages that share a single common code + interface layer: - -| Unit | Surface | Sourced from | First version | -|---|---|---|---| -| `skyflow-js` (package) | **privacyDB** (existing public API) | `main` | continues current semver | -| `skyflow-flowvault-js` (package) | **flowDB** (Flow Vault `/v2` API) | `2.9.0-beta.1` tag deltas | `1.0.0` | -| `core/` (shared **folder**, not a package) | shared code + interfaces | common ancestor of both | n/a — compiled into each package | - -The organizing principle: **create common code & interfaces both SDKs use, and implement flowDB by extending those common things** — not by branching inside shared code. There is no runtime `isFlowDB` toggle in the target design; the variant is fixed per package at build time. - -The shared code lives in a plain `core/` **folder** (not an npm package): both SDK packages import it via a `@core` path alias, it is compiled into each package's bundle at build time, and a boundary lint rule keeps it variant-neutral (see §3.1). Only the two SDKs are packages, because only they are published. - -This reframes the split as a **three-way factoring of a common ancestor**: - -- `core/` ≈ infra present nearly identically in both `main` and the `2.9.0-beta.1` flowDB tag. -- `skyflow-js` ≈ `main` minus core. -- `skyflow-flowvault-js` ≈ the `2.9.0-beta.1` flowDB additions minus core, expressed as core extensions. - ---- - -## 2. Key findings that shape the plan - -1. **The npm tarball is small; the expensive shared runtime is already externalized.** `package.json` ships only `dist/sdkNodeBuild` (UMD node bundle) + `types/`. The browser bundle (`dist/v1`) and the **entire Elements iframe runtime** (`dist/v1/elements`, built from `src/index-internal.ts`) deploy to S3/CloudFront and load at runtime via `IFRAME_SECURE_SITE` / `customElementsURL`. The largest shared asset never enters the package boundary. - -2. **The data layer already uses a variant-adapter pattern.** `core-utils/collect.ts` and `reveal.ts` are structured around `IInsertVariant` / `IDetokenizeVariant` strategy objects (`flowDBInsertVariant`, `flowDBDetokenizeVariant`) fed into a shared `executeInsert(variant, …)` / `executeDetokenize(variant, …)`. flowDB's adapter is built; privacyDB's is restored from `main`. - -3. **There is no runtime variant flag anywhere.** Selection today is authoring-time: in `core-utils`, by which wrapper the caller imports (`insertDataInCollect` vs `insertDataInCollectFlowDB`); everywhere else the code unconditionally calls the flowDB wrapper. Per-package builds make this compile-time-fixed, which is cleaner. - -4. **The flowDB source is not a clean privacyDB baseline.** In `2.9.0-beta.1` the privacyDB *element* paths were replaced by flowDB, legacy privacyDB type exports are commented out, and many privacyDB tests are `skip`ped. Because `skyflow-js` is sourced from `main`, this is handled by construction rather than manual restoration. - -5. **`webpack.common.js` is package-agnostic.** No package name, output dir, or iframe URL is hardcoded in it; only `entry`, `output.path`, and the UMD `library` name differ per artifact. `webpack.dev.js` already runs multiple entry points through one config. - ---- - -## 3. Target architecture - -``` -repo/ - core/ # SHARED SOURCE — plain folder, NOT a package. Imported via @core alias, - # compiled into each package's bundle at build time. - - iframe skeleton + variant-neutral frame leaf-helpers - - event bus (event-emitter), framebus wrapper (libs/bus), bus-events - - iframer, JSS (jss-styles), metrics - - pure validators (card / Luhn / regex / format), validateElements, getUnformattedValue - - logs + error-codes (utils/constants), logs-helper, jwt-utils - - constants (event protocol, styles, card infra, element metadata, ElementType/CardType) - - neutral common types + BASE element-input / response-envelope interfaces - - skyflow-error BASE class - - uuid / regex / deep-clone - - index.ts barrel — the surface both SDKs consume - - packages/ - skyflow-js/ # privacyDB (from main). name: "skyflow-js" - - index.ts / index-node.ts (PDB public surface, incl. /v1 response types) - - index-internal.ts → builds PDB iframe → hosted at PDB URL - - /v1 data layer (privacyDB collect/reveal/get/getById/delete builders) - - PDB element input/response types extending @core bases - - PDB upsert options { table, column } - - PDB frame controller — owns its own tokenize/revealData, calls @core leaf helpers - - skyflow-flowvault-js/ # flowDB (from 2.9.0-beta.1 tag). name: "skyflow-flowvault-js", v1.0.0 - - index.ts / index-node.ts (flowDB public surface) - - index-internal.ts → builds flowvault iframe → hosted at flowvault URL - - /v2 data layer (flowDB insert/update/detokenize builders + parsers) - - flowDB element input/response types extending @core bases - - flowDB upsert options { tableName, uniqueColumns, updateType } - - skyflow-flowdb-error (extends @core error base) - - flowDB frame controller — owns its own tokenize/revealData (incl. cvvMap), calls @core leaf helpers - - tsconfig.base.json # @core/* → core/* (single source of truth for the alias) - .eslintrc.js # boundary rule: core/ may not import from packages/* -``` - -Each SDK package builds all three artifacts (browser, node, **its own** iframe) from `@core + its variant`. `core/` is a shared source folder, not a package: it is compiled into each package's bundle at build time (nothing is published or installed separately, so consumers still install one self-contained package with no peer dependency). - -### 3.1 Packaging strategy: monorepo with workspaces - -Chosen over the alternatives: - -| Option | Bundle-size risk for existing skyflow-js | Sync burden | Verdict | -|---|---|---|---| -| **Monorepo + workspaces (chosen)** | None — each SDK bundles only its variant + core | Low — one source of truth, atomic cross-cutting PRs | ✅ | -| Single build, 2 entry points, tree-shake | High/uncertain — interleaved `core-utils` can't tree-shake apart; UMD/IIFE tree-shakes poorly | Low | ✗ doesn't deliver two publishable packages | -| Two repos + shared scoped package | None | High — shared code drifts, cross-repo PRs | ✗ sync cost during fast-moving flowDB beta | - -**Versioning:** independent semver per SDK (`skyflow-js` continues its line; `skyflow-flowvault-js` starts at `1.0.0`). The `core/` folder has no version of its own — it is source compiled into each package. Same npm dist-tag scheme across both SDKs. - -**Shared core is a folder, not a package.** Only the two publishable SDKs are packages (each needs its own `package.json` `name`/`version`). The shared code is a plain `core/` folder consumed by both via a `@core` path alias, with two guardrails: - -- **Path alias (ergonomics)** — one source of truth in `tsconfig.base.json` `paths` (`@core/* → core/*`), mirrored to `webpack` `resolve.alias` and `jest` `moduleNameMapper`. Keeps imports clean (`@core/validators`, not `../../../core/...`) and relocation-safe. Because `core/` is not under `node_modules`, `babel-loader`'s `exclude: /node_modules/` does not skip it — it compiles normally, so **no build-ordering step and no separate core build.** -- **Boundary rule (enforcement)** — `import/no-restricted-paths` (`eslint-plugin-import`) forbids `core/**` from importing `packages/**`, so core stays variant-neutral. This is the actual enforcement; the alias does not enforce. (A folder relies on this lint rule where a package would add only a partial resolution-level barrier — which would still need the same rule for relative reach-arounds — so the folder loses no practical enforcement.) - -The two SDKs still live under npm **workspaces** purely for dev ergonomics (one `npm install`, shared devDeps, unified lint/test) — the shared *code* is just not one of the workspace packages. - -### 3.2 Iframe strategy: separate builds, single codebase - -**Separate iframe *artifacts*, single iframe *codebase*.** Each package builds its own iframe from the shared core skeleton + its own variant data layer, and hosts it at its own URL. - -Rationale: - -- The variant becomes **compile-time fixed** per iframe — no runtime adapter/branch inside the iframe. This *deletes* the hardest refactor item (injecting a data adapter into a shared controller). -- A flowDB data-layer change **cannot** break the privacyDB iframe — they are different artifacts. -- Each iframe bundle is smaller (one variant's builders). -- **Version-skew isolation:** each package owns both its client SDK and its hosted iframe, versioned together. A shared iframe would instead have to stay compatible with two independently-versioned SDKs at once — a worse compatibility surface. - -**How the iframe URL reaches the client (existing mechanism, replicated per package).** `properties.ts` reads `process.env.IFRAME_SECURE_ORIGIN`/`IFRAME_SECURE_SITE`; [webpack.common.js](../webpack.common.js) `DefinePlugin` inlines them at build time; the release workflow supplies them as `IFRAME_SECURE_SITE: "v${RELEASE_VERSION}/${secret}"` + `IFRAME_SECURE_ORIGIN: ${secret}`. So each build bakes an absolute URL `${ORIGIN}/v${version}/${SITE}` into the bundle; `getIframeSrc()` returns it, and `customElementsURL` can still override at runtime. To produce the two builds, each package's release workflow supplies its own origin/site secrets + version: - -| Package | `IFRAME_SECURE_ORIGIN` | `IFRAME_SECURE_SITE` | Baked URL (example) | -|---|---|---|---| -| `skyflow-js` | `https://js.skyflow.dev` | `elements/index.html` | `js.skyflow.dev/v2.7.9/elements/index.html` | -| `skyflow-flowvault-js` | `https://js-flowvault.skyflow.dev` | `elements/index.html` | `js-flowvault.skyflow.dev/v1.0.0/elements/index.html` | - -Same S3 bucket / different sub-root folder is fine — the bundle only cares about the final absolute URL. The two distinct **origins** are also what framebus uses for `postMessage` targeting (`.target(IFRAME_SECURE_ORIGIN)`), so separate hosts strengthen cross-package isolation. **Guardrail:** keep the committed `process.env`-based `properties.ts`; the working-tree localhost hardcode is local-dev only and must never be committed or it breaks prod iframe loading. Infra to-do (not SDK): map the `js-flowvault.skyflow.dev` host (CloudFront/CNAME) to its S3 sub-folder. - -Costs (accepted, low): - -- Two S3 uploads + two CloudFront distributions/invalidations — CI config, not code. -- Shared core code is duplicated inside both hosted iframe bundles — CDN assets, not user installs; a cache/bandwidth non-issue. -- The two `index-internal.ts` entries must stay structurally parallel — mitigated by core owning the skeleton so the entries are thin. - -**Non-negotiable:** "separate builds" must not become "fork the iframe code." ~90% of the iframe runtime (DOM building, element rendering, JSS, framebus protocol, frame lifecycle/height/ready handshake) is variant-neutral and lives once in `core/`. Core exposes that skeleton plus variant-neutral leaf helpers; each package composes them into its own frame controller (see §3.4 — helpers, not a callback base). - -### 3.3 Element inputs & responses: separate types, common rendering - -Three-way split: - -1. **Element rendering / DOM / event protocol → common (core).** Framebus event *names* (`COLLECT_CALL_REQUESTS`, `REVEAL_CALL_REQUESTS`, `COMPOSABLE_REVEAL`, etc.) live in core constants; variant-neutral leaf helpers — frame/element lookup, `getUnformattedValue`, checkbox concatenation, duplicate-element detection, and the element **validation pass** — become core helpers both packages call. (Note: `cvvMap` / mock-CVV token masking is **flowDB-only**, not a core concern.) - -2. **Element input & response *types* → separate, extending common bases.** They genuinely diverge: - - Reveal input: PDB `IRevealElementInput` (`skyflowID/table/column/redaction`) vs flowDB `IFlowDBRevealElementInput` (token-only + `tokenGroupRedactions`). - - Response envelope: flowDB unified `records[]` (`{tableName, skyflowId, tokens, hashedData, httpCode, error}`) vs PDB per-operation responses. - - Upsert options: PDB `{table, column}` vs flowDB `{tableName, uniqueColumns, updateType}` — confirmed different meanings. - - Core defines **base interfaces** for shared element/style fields and a base record/response envelope; each package **extends** them. Public names (`RevealElementInput`, `CollectResponse`, `UpsertOptions`, `SkyflowError`) then resolve to the correct shape per package automatically. - -3. **Parse/build logic for those structures → per-package** (in each package's data layer, and therefore in each package's iframe build): `construct*Request`, `formatRecordsForClient*`, `constructFlowDB*Response` are variant-specific and stay out of core. - -### 3.4 Frame-controller decomposition - -Applying §3.2/§3.3 to the frame controllers concretely: **core exposes only variant-neutral leaf helpers; each package owns its own `tokenize()` / `revealData()` end-to-end.** No template-method base that calls back into subclass hooks — that callback inversion is the "complex coupling with core" to avoid. Once `cvvMap` (flowDB-only), request keys, and the response envelope all differ per variant, the assembly loop is **genuinely different code, not duplication**; only the frame-iteration + validation pass is structurally identical, and it becomes one shared helper. - -Split of `tokenize()` ([skyflow-frame-controller.ts:610-813](../src/core/internal/skyflow-frame/skyflow-frame-controller.ts#L610-L813)): - -| Region | Lines | Owner | -|---|---|---| -| Validate elements | 617-651 | **core** — `validateElements(options, ctx) → errorMessage` helper (touches no request/response/cvv shape) | -| Assemble insert/update buckets | 653-743 | **per package** — keys differ; `cvvMap` branches are flowDB-only | -| Build request | 744-757 | **per package** — `constructFlowDBInsertRequest` etc. | -| Send + parse response | 763-806 | **per package** — `cvvMap` masking + response envelope are variant-specific | - -Core additionally supplies leaf primitives both packages call: frame/element lookup, `getUnformattedValue`, checkbox concatenation, duplicate-element detection. The same split applies to the second `tokenize()` in [frame-element-init.ts:299-504](../src/core/internal/frame-element-init.ts#L299-L504) and to `revealData`. Net: loose coupling **and** near-zero true duplication (only the validation pass is shared, via a plain function call — no inheritance). - ---- - -## 4. Coupling map (current single tree) - -Classification of the interleaved modules, for reference during extraction: - -| File | Nature | Action | -|---|---|---| -| `core-utils/collect.ts` | PDB + flowDB builders interleaved; `constructElementsInsertReq` + `replaceCVVTokensInResponse` (CVV is flowDB-only) mixed with variant transport | **Cut** — request/response builders per package; validators to core | -| `core-utils/reveal.ts` | 3 seams: flowDB detokenize/reveal, PDB `/v1` GET/getById/render-file, formatters | **Cut** (hardest file) — parse/format per package | -| `skyflow-frame-controller.ts` | one iframe brain for `/v1` pure-JS + flowDB elements; `tokenize()` = neutral validation + variant assembly/request/response | Each package owns its own `tokenize()`; core supplies leaf helpers only (see §3.4) | -| `frame-element-init.ts` | flowDB-only composable collect; neutral validation + variant assembly/request | Each package owns its own `tokenize()`; core leaf helpers only | -| `composable-frame-element-init.ts` | flowDB-only reveal | Move to flowvault | -| `internal-types/index.ts` | neutral internal types + ~25 flowDB interfaces + `CollectResponse`/`RevealResponse` | **Split** (neutral → core, flowDB → flowvault) | -| `libs/skyflow-flowdb-error.ts` | standalone flowDB error class (currently the public `SkyflowError`) | Move to flowvault; extend core error base | -| `libs/skyflow-error.ts` | neutral error base | Move to core | -| `collect-container.ts`, `reveal-container.ts`, `composable-reveal-container.ts` | variant-neutral except `SkyflowFlowDBError` import + `IFlowDBRevealElementInput` type | Container logic → core base; error/type per package | -| `skyflow.ts` | one `Skyflow` class + `container()` factory serves both | Core base + per-package entry | -| `properties.ts` | plain constants (iframe URL defaults) | Per-package (each sets its own iframe URL default) | -| `utils/common/index.ts` | neutral types **+** one flowDB import (`IFlowDBUpsertOptions`) | Neutral → core; invert the flowDB back-edge | -| `index-node.ts` | public names aliased onto flowDB impls | Per-package index | - -**Layering back-edges to invert (all type-only, mechanical) before extraction:** - -1. `utils/common/index.ts:4` → `IFlowDBUpsertOptions` from `core-utils/collect` (flowDB name in shared `ICollectOptions`). Move upsert-options types into core; make `common` variant-free. -2. `utils/helpers` & `utils/validators` → `reveal-container` + `skyflow.ts` input types. Relocate referenced interfaces into core types. -3. `libs/element-options` → `collect-element` / `compose-collect-element`. -4. `internal-types` → the `index-node` barrel + `external/skyflow-container`. -5. **The structural edge:** the four frame controllers hard-import flowDB builders from `core-utils`. Resolved by moving variant-neutral leaf helpers to `core/` and letting each package own its own controller (§3.4). - -**Cleanly shared today (no back-edges, no flowDB) → core as-is:** `event-emitter`, `libs/bus`, `bus-events`, `iframer`, `jss-styles`, `utils/logs`, `logs-helper`, `utils/constants` (error codes), `jwt-utils`, `libs/skyflow-error`, `uuid`/`regex`/`deep-clone`, `metrics`, `core/constants`, and the neutral bulk of `utils/common`. - ---- - -## 5. Migration phases (sequencing) - -### Phase 0 — Baseline -- Confirm the `main` privacyDB baseline and the `2.9.0-beta.1` flowDB delta set as the two source inputs. - -### Phase 1 — Establish `core/` from `main` (privacyDB only) — critical path -On an integration branch cut from `main`, extract the variant-neutral code into `core/` (behind the `@core` alias + boundary lint) and leave the existing `skyflow-js` (privacyDB) working on top of it. **No flowDB and no second package yet** — `main` has no flowDB to carve, and flowvault can't extend a `core/` that doesn't exist. Pure internal refactor: public surface + telemetry unchanged throughout. -- Scaffold `core/` + `@core` alias (tsconfig/webpack/jest) + `import/no-restricted-paths` (`core/` ⇏ `src/`). -- Move neutral leaves → `core/` (uuid/regex/bus/jss/logs/constants/iframer/metrics/jwt), then telemetry-identity injection, then neutral common types + **base interfaces**. -- Invert the `main` back-edges (`validators`/`helpers`/`element-options`/`internal-types` → container/`skyflow.ts` types) by relocating the referenced interfaces into `core/types`. -- Extract the variant-neutral **leaf helpers** (`validateElements`, `getUnformattedValue`, frame/element lookup, checkbox concat, duplicate detection) into `core/`; keep the pure-JS transport neutral. **No template-method base with subclass callbacks** (see §3.4). -- Carve `core-utils/collect.ts`/`reveal.ts`: neutral assembly helpers → `core/`; privacyDB `/v1` builders/parsers stay in `skyflow-js`. - -→ Full task-by-task breakdown in [phase-1-execution-plan.md](phase-1-execution-plan.md). - -### Phase 2 — Physical split + build flowvault from `2.9.0-beta.1` -- Relocate the existing tree into `packages/skyflow-js`; set up npm workspaces for the two SDK packages; keep `core/` at the root. -- **Build `skyflow-flowvault-js` from the `2.9.0-beta.1` flowDB deltas as `@core` extensions:** flowDB element input/response types extending the core bases, flowDB `/v2` data layer (incl. `cvvMap` masking), `skyflow-flowdb-error` extending the core error base, and flowvault's own `tokenize()`/`revealData()` calling the core leaf helpers. Broaden the telemetry language-label check to treat `skyflow-flowvault-js` as `JS`. -- Per-package `index.ts` / `index-node.ts` with correct public names (§6); per-package `package.json` (name / main / types / files). -- Per-package browser + node + iframe webpack configs, each `merge`-ing shared `webpack.common.js`; flowvault's UMD `library` + browser IIFE global = `SkyflowFlowVault` (skyflow-js keeps `Skyflow`). - -### Phase 3 — Build & release -- Parameterize `common-release.yml` with inputs (`PACKAGE_DIR`, `PACKAGE_NAME`, `S3_PREFIX`, iframe URL / `BUILD_IFRAME`); add a thin caller workflow per package. -- Two iframe deploy pipelines (one per package), each to its own S3 prefix + CloudFront. -- Untangle tests (§7). - -### Phase 4 — Samples & docs -- Repoint `samples/using-script-tag/*.html` and `samples/using-typescript/skyflow-elements` at the correct package/CDN URL. -- Move flowDB samples (currently on preview CDN URLs in `using-script-tag`) under the flowvault package. -- Update README(s) per package. - -Critical path is the Phase 1 `core-utils` factoring and validating both iframes against the shared core. The iframe-deploy config cost in Phase 3 is offset by the deleted adapter-injection work in Phase 1. - ---- - -## 6. Backward-compatibility guarantees (`skyflow-js` consumers) - -Because `skyflow-js` is sourced from `main`, its public surface is restored by construction. The guarantee, by tier: - -- **Safe verbatim:** `Skyflow` (default), `CollectContainer/Element`, `Composable*`, `RevealContainer/Element`, `ComposableReveal*`; common types `ContainerOptions`, `CollectElement*`, `CollectOptions`, `AdditionalFields*`, `CardMetadata`, `Input/Label/ErrorTextStyles`, `RedactionType`, `RenderFileResponse`, `ValidationRule(Type)`, `EventName`, `LogLevel`, `Env`, `ElementState`, `ErrorType`, `ErrorMessages`, `UpdateType`, `CardType`, `ElementType`, `ContainerType`, `SkyflowConfig`. -- **Names that must resolve to the PDB shape** (flowDB-bound in `2.9.0-beta.1`, PDB-bound in `skyflow-js`): `CollectResponse`, `CollectRecord`, `RevealResponse`, `RevealRecord`, `RevealElementInput` (regains `skyflowID/table/column`), `RevealOptions`, `RevealElementOptions`, `UpsertOptions` (back to `{table, column}`), `SkyflowError` (neutral `libs/skyflow-error`, not the flowDB class). -- **Restored from `main`:** `InsertResponse`, `UpdateResponse`, `DetokenizeResponse`, `GetResponse`, `GetByIdResponse`, `DeleteResponse`, `UploadFilesResponse` + their request/record types. - -**Canary consumer:** `samples/using-typescript/skyflow-elements/src/index.ts` imports exactly the collision-tier names (`CollectResponse`, `RevealResponse`, `RevealElementInput`, `RevealOptions`, `SkyflowError`) — use it as the compat smoke test for `skyflow-js`. - -`skyflow-flowvault-js` carries the flowDB shapes of these same names — no backward-compat constraint (new package, v1.0.0). - ---- - -## 7. Build & release detail - -- **Webpack:** share `webpack.common.js` (add the `@core` `resolve.alias`; the existing `babel-loader` `exclude: /node_modules/` already compiles `core/` since it is a plain folder — no change needed there). Each package needs only small browser + node + iframe configs differing in `entry`, `output.path`, and UMD `library` name. The iframe config is per-package (each imports the core skeleton via its `index-internal.ts`). -- **Shared-core wiring:** `@core` alias declared once in `tsconfig.base.json` and mirrored to webpack + jest; `import/no-restricted-paths` enforces `core/` ⇏ `packages/`. `core/` is compiled inline into each build — no separate core build, no build ordering. -- **SDK telemetry identity:** inject `SDK_NAME`/`SDK_VERSION` per package via `DefinePlugin` (from each package's own `package.json`); the shared `core/` helper reads these constants instead of importing `package.json`. Keep the language label `JS` for both SDKs by broadening the `sdkName === 'skyflow-js'` check to include `skyflow-flowvault-js`; preserve the existing `metaData` wrapper-override so a wrapper like `skyflow-react@x` still reports `React`. `skyflow-js` output stays byte-identical (`skyflow-js@`, `JS SDK v`). -- **Hardcoded values to watch:** output dirs (`dist/v1`, `dist/sdkNodeBuild`, `dist/v1/elements`), the UMD/IIFE global (`Skyflow` for skyflow-js, `SkyflowFlowVault` for flowvault), and the iframe URL default in each package's `properties.ts`. -- **Release workflow:** `common-release.yml` is already `workflow_call` but hard-assumes one `package.json`, one `dist/v1` S3 prefix, one `npm publish`. Parameterize by package/dir/prefix; per-package caller workflows. **Tags:** skyflow-js keeps bare-semver triggers; flowvault uses `flowvault-`-prefixed tags (configurable via the workflow trigger pattern). **Toolchain:** `setup-node@v4` / Node 18 across CI. -- **npm publish** keys off `package.json` `name`/`version`; each package publishes independently under its own name. - -## 8. Testing - -- `jest.config.json` is minimal: `jsdom`, `collectCoverage: true`, no `roots`, no `coverageThreshold`, only an svg mock. **Per-package coverage config is written from scratch.** -- The 5 dedicated `*.flowdb.test.js` files move mechanically to flowvault: - - `tests/core-utils/collect.flowdb.test.js`, `tests/core-utils/reveal.flowdb.test.js` - - `tests/core/internal/frame-element-init.flowdb.test.js`, `.../composable-frame-element-init.flowdb.test.js` - - `tests/core/internal/skyflow-frame/skyflow-frame-controller.detokenize.flowdb.test.js` -- The PDB test files are **half-migrated** in `2.9.0-beta.1` (flowDB `skip`s + inline `SkyflowFlowDBError` assertions in `skyflow.test.*`, `collect-container`, `reveal-container`, `skyflow-frame-controller`, `frame-element-init.*`, `upload-tokenize`). Since `skyflow-js` is sourced from `main`, take its tests from `main` rather than untangling the beta versions. -- Note many tests are duplicated `.js` + `.ts` — deduplicate opportunistically during the split. - ---- - -## 9. Pre-execution prerequisites (blocker checklist) - -Grouped by when each must be settled. Status reflects decisions taken so far. - -### Resolved / retired (for the record) -- ✅ **Versioned iframe URL delivery** — resolved. Existing `properties.ts` + `DefinePlugin` + release-workflow secrets bake a per-package absolute URL; each package supplies its own origin/site/version (see §3.2). -- ✅ **Workspace transpilation gotcha** — retired by the shared-`core/`-folder decision. Because `core/` is a plain folder (not symlinked under `node_modules`), `babel-loader` compiles it normally; no Option A/B, no build ordering (see §3.1). -- ✅ **Per-package SDK telemetry identity** — resolved. Inject `SDK_NAME`/`SDK_VERSION` per package via `DefinePlugin` from each package's own `package.json`; the shared `core/` helper reads the injected constants instead of `import …/package.json`. flowvault reports `skyflow-flowvault-js@` (no analytics enum to register). Language label stays `JS` for both first-party SDKs — broaden the `sdkName === 'skyflow-js'` check ([helpers/index.ts:476](../src/utils/helpers/index.ts#L476)) to also accept `skyflow-flowvault-js`, leaving the `metaData` wrapper-override (`skyflow-react@x` → `React`) intact. `skyflow-js` telemetry output is unchanged. See §7. -- ✅ **Pure-JS surface ownership** — resolved. `skyflow-flowvault-js` v1.0.0 is **elements-only**; pure-JS `Skyflow.*` methods are deferred to a future release. This is **not a public removal**: on `2.9.0-beta.1` the pure-JS methods are already private (`#insert`/`#detokenize`/… in [skyflow.ts:301-339](../src/skyflow.ts#L301), no public alias; pure-JS types commented out in `index-node.ts`), so flowDB pure-JS was never publicly exposed. `skyflow-js` keeps its pure-JS methods **public** (restored from `main`, where they are public). **`/v2` file-upload/render → `skyflow-js` only**; flowvault has no file support. To keep future flowvault pure-JS additive, keep the pure-JS **transport** (the `PUREJS_REQUEST` dispatch + container→frame-controller flow) variant-neutral in `core/`; only the per-method `/v1` vs `/v2` data layer is package-specific. **Phase-1 verify:** skyflow-js's file-upload endpoint must land on `main`'s behavior, not the beta's `/v2` variant. -- ✅ **3DS / ThreeDS ownership** — resolved. Lives entirely in `skyflow-js`: public on `main` (`ThreeDS` + `ThreeDSBrowserDetails`, [index-node.ts:57,75](../src/index-node.ts)), restored as-is along with the `threeds.ts` module. flowvault has **no** 3DS now (commented out in `2.9.0-beta.1`, never publicly exposed there); a future flowvault 3DS would be additive. 3DS is a package-specific feature, **not** shared `core/`. - -_All Phase-1-blocking decisions are now resolved._ - -### Before Phase 2 (settled) -- ✅ **Toolchain bump for workspaces.** Build/CI moves to **Node 18 LTS** + `setup-node@v4` (from Node 14.17.6). This is a *build/dev* change only — the published `engines` (`node >=12`) stays, so **consumers on older Node are unaffected**. -- ✅ **Git/branch strategy.** Cut a dedicated **integration branch from `main`**; all refactor work targets that branch and merges into it **phase by phase** (not directly to `main`). Use `git mv` for the physical move so `git blame`/`--follow` history is preserved. The integration branch becomes the release source once complete. - -### Before Phase 3 (settled) -- ✅ **Tag namespaces.** `skyflow-js` keeps its existing bare-semver tags (no change to current automation); `skyflow-flowvault-js` uses **`flowvault-`-prefixed** tags (`flowvault-v1.0.0`, `flowvault-v1.0.0-beta.1`) with its own caller workflows filtering `flowvault-*`. The prefix lives only in the workflow trigger pattern — **configurable later** via a one-line workflow edit, no artifact impact. -- ✅ **npm name + publish rights.** Confirmed — publish access ready in dev + prod under the same npm org. -- ✅ **UMD/global name** for flowvault browser bundle = **`SkyflowFlowVault`** (consumer-facing global for script-tag/CDN users; skyflow-js keeps `Skyflow`). Applies to both the UMD `library` name and the browser IIFE (`window.SkyflowFlowVault`) and to flowvault's script-tag samples. -- ✅ **Consumer migration comms — not needed.** The `2.9.0-beta.1` flowDB build was shared privately with a single customer, not published to public npm — so there is no public consumer to migrate and no npm deprecation required. -- ⏳ **Infra request (external — infra team).** Provision flowvault SDK + iframe hosting, mirroring the existing skyflow-js setup so the team can correlate. Hand-off checklist: - 1. **DNS host** — `js-flowvault.skyflow.dev` (prod) + dev/sandbox equivalents matching skyflow-js's per-env hosts. - 2. **TLS** — ACM certificate covering the new host(s). - 3. **CDN** — CloudFront distribution for the new host; **origin = the same S3 bucket** as skyflow-js under a dedicated `flowvault/` sub-prefix; serve `/{version}/elements/*` (iframe) and `/{version}/*` (browser bundle); mirror skyflow-js's cache behaviors. - 4. **S3 write** — grant the CI release role write access to the `flowvault/` prefix. - 5. **Invalidation** — the flowvault CloudFront **distribution ID** + IAM permission for the CI role to invalidate it (skyflow-js's release runs ×10 invalidations). - 6. **CI secrets** (per env, mirroring skyflow-js's `PROD_/SANDBOX_` secrets): - - `FLOWVAULT_{PROD,SANDBOX}_IFRAME_SECURE_ORIGIN` = `https://js-flowvault.skyflow.dev` - - `FLOWVAULT_{PROD,SANDBOX}_IFRAME_SECURE_SITE` = `elements/index.html` - - the flowvault CloudFront distribution ID (for invalidation). - -### Verification gates (settled) -- ✅ **Bundle-size.** One-time check (not a CI gate): capture `skyflow-js`'s current bundle size from `main` as the baseline and compare the post-split `skyflow-js` once. -- ✅ **Coverage.** Floor = the current `main`-branch coverage per package (no backsliding), applied via per-package `coverageThreshold` after the test split. diff --git a/docs/phase-1-execution-plan.md b/docs/phase-1-execution-plan.md deleted file mode 100644 index ffb0192c..00000000 --- a/docs/phase-1-execution-plan.md +++ /dev/null @@ -1,201 +0,0 @@ -# Phase 1 — Detailed Code Execution Plan (`core/` extraction) - -Companion to [package-split-plan.md](package-split-plan.md). This decomposes **Phase 1** into small, individually-reviewable tasks. Each task is one PR into the integration branch, is **behavior-preserving**, and leaves the build + tests green. - ---- - -## Scope & non-goals - -**In scope (Phase 1):** On an integration branch cut from `main` (privacyDB baseline), extract the variant-neutral code into a shared `core/` folder behind the `@core` alias + boundary lint rule, and leave the existing `skyflow-js` (privacyDB) package working on top of it. - -**Explicit non-goals (deferred to later phases):** -- **No flowDB.** `main` has no flowDB code; flowvault is built from the `2.9.0-beta.1` deltas as `@core` extensions in a **later phase** (it cannot extend a `core/` that doesn't exist yet). -- **No second package / no `packages/` relocation yet.** During Phase 1 the existing tree stays at the repo root as the `skyflow-js` package; `core/` is added alongside it. Creating `packages/skyflow-js` + `packages/skyflow-flowvault-js` and npm workspaces is Phase 2. - -**Invariant after every task:** the public API surface (`index.ts` / `index-node.ts` exports and their shapes), SDK telemetry output, and all existing tests are **unchanged**. Phase 1 is a pure internal refactor. - -### End state of Phase 1 -``` -repo/ - core/ # variant-neutral shared source + @core barrel - src/ # existing skyflow-js (privacyDB), now importing @core - tsconfig.base.json # @core/* → core/* - .eslintrc(.js) # import/no-restricted-paths: core/ ⇏ src/ -``` - ---- - -## Working model - -- **Branch:** one long-lived integration branch off `main` (e.g. `refactor/pkg-split`). Every task below is a PR **into that branch**, reviewed and merged before the next starts. The branch becomes the release source when the whole split is done. -- **One task = one PR.** Ordered; each builds on the previous. Earliest tasks are lowest-risk (pure moves); boundary-inversion tasks come after the leaves are in place. -- **Moves preserve history:** use `git mv` so `git blame`/`--follow` still work. - -### Verification recipe (run at the end of every task) -1. `npm run type-check` — no TS errors. -2. `npm test` — full suite green (no new skips). -3. `npm run build-browser-sdk && npm run build-node-sdk && npm run build-iframe` — all three bundles build. -4. **Public-surface guard:** `npm run build:types` and diff the emitted `types/index-node.d.ts` + `types/index.d.ts` against the pre-Phase-1 snapshot — expect **zero diff** (Tasks that intentionally relocate a type must still produce an identical *exported* shape). -5. **Consumer canary:** `samples/using-typescript/skyflow-elements` still type-checks against the built package. -6. **Boundary lint:** `npx eslint` passes, including `import/no-restricted-paths` (warn until Task 1.11, then error). - -### Definition of done (per task) -Verification recipe green · reviewer approves · no change to public surface or telemetry (except where a task explicitly restructures internals with an identical external shape). - ---- - -## Task list - -### Task 1.0 — Branch, toolchain, empty boundary scaffolding -**Goal:** stand up the `@core` boundary with nothing moved yet. -- Cut `refactor/pkg-split` from `main`. Capture the baselines: `build:types` snapshot (for the surface guard), current bundle size (webpack-bundle-analyzer), current coverage numbers. -- CI: `actions/setup-node@v4`, Node 18 (build/dev only; leave published `engines` untouched). -- Create empty `core/` + `core/index.ts` (empty barrel). -- Add the `@core` alias in **one source of truth** (`tsconfig.base.json` `paths: { "@core/*": ["core/*"] }`) mirrored to `webpack.common.js` `resolve.alias` and `jest.config.json` `moduleNameMapper`. -- Add `eslint-plugin-import` `import/no-restricted-paths` zone (`core/` ⇏ `src/`), severity **warn** for now. - -**Reviewability:** config-only diff; no logic touched. -**Verify:** recipe green; surface snapshot captured. - ---- - -### Task 1.1 — Move zero-import neutral leaves -**Goal:** move the leaves that import nothing (safest first). -- `git mv` → `core/`: `libs/uuid.ts`, `libs/regex.ts`, `libs/deep-clone.ts`, `utils/jwt-utils/`, `libs/jss-styles.ts`, `event-emitter/`, `libs/bus.ts`. -- Repoint importers to `@core/...`; add these to the `core/index.ts` barrel. - -**Reviewability:** pure move + import rewrite; small. -**Verify:** type-check + tests green. - ---- - -### Task 1.2 — Move logging, error-codes, DOM/iframe & metrics primitives -**Goal:** move the neutral, downward-only utility tier. -- `git mv` → `core/`: `utils/logs.ts`, `utils/constants.ts` (`SKYFLOW_ERROR_CODE`), `core/constants.ts`, `properties.ts`, `iframe-libs/iframer.ts`, `utils/bus-events/`, `metrics/`. -- `utils/logs-helper/` moves too, **but** it depends on `helpers.getSDKLanguageAndVersion` — pull that one neutral helper across with it (or temporarily import from `src`) and finish the helpers move in Task 1.3. - -**Reviewability:** move + import rewrite; watch the `logs-helper → helpers` edge. -**Verify:** recipe green. - ---- - -### Task 1.3 — SDK telemetry identity injection + neutral helpers -**Goal:** make SDK name/version injectable per package (prerequisite for two packages) with **identical** output for skyflow-js. -- Replace `import SDKDetails from '../../../package.json'` ([helpers/index.ts:17](../src/utils/helpers/index.ts#L17)) with build-time `SDK_NAME` / `SDK_VERSION` injected via `DefinePlugin` (mirroring the existing `IFRAME_SECURE_*` injection); the shared helper reads the injected constants. -- Restructure the language-label ([helpers/index.ts:476](../src/utils/helpers/index.ts#L476)) so it's injection-ready (skyflow-js → `JS`), preserving output. (The `=== 'skyflow-js'` broadening for flowvault happens when flowvault is built — not now.) -- Move the remaining neutral helper functions into `core/helpers`. - -**Reviewability:** localized to helpers + webpack define; call out the telemetry-preserving intent. -**Verify:** recipe green **plus** an explicit telemetry check — `sdk_name_version` still equals `skyflow-js@` and the label `JS SDK v` (snapshot before/after). - ---- - -### Task 1.4 — Neutral common types → `core/types` + base interfaces -**Goal:** carve the neutral type surface out of `utils/common` and define the base interfaces packages will extend. -- Move to `core/types`: enums (`EventName`, `LogLevel`, `Env`, `RedactionType`, `UpdateType`, `ValidationRuleType`, `ErrorType`, `MessageType`, `RequestMethod`), element/style types (`Style`, `ContainerOptions`, `Input/Label/ErrorTextStyles`, `CollectElement*`, `ICollectOptions`, `ElementState`, `AdditionalFields*`, `CardMetadata`), and the neutral record/response families. -- Define **base interfaces**: base element-input, base record/response envelope, base upsert options — the extension points for privacyDB (now) and flowDB (later). -- Keep privacyDB-specific types in `src`, extending the base. -- Re-export everything from `index-node.ts`/`index.ts` under the **same public names**. - -**Reviewability:** larger but mechanical; the surface guard is the safety net. -**Verify:** recipe green; **surface diff must be zero** (same exported names + shapes); using-typescript canary compiles. - ---- - -### Task 1.5 — Invert back-edge: validators -**Goal:** stop shared validators importing "up" into container/`skyflow.ts` types. -- Today `validators/index.ts` imports `IRevealElementInput` (`reveal-container`) and `ISkyflow` (`skyflow.ts`). -- Split: pure validators (card/Luhn/regex/format) → `core/validators`. For request/shape validators that reference input types, **relocate those input interfaces into `core/types`** (Task 1.4's base types) so the dependency points down, not up. - -**Reviewability:** focused on one file + the relocated interfaces. -**Verify:** recipe green; boundary lint clean for the moved validators. - ---- - -### Task 1.6 — Invert back-edges: helpers & element-options -**Goal:** clear the remaining shared→container type edges. -- `helpers/index.ts` imports `IRevealElementOptions` (`reveal-container`) + `ContainerType`/`ISkyflow` (`skyflow.ts`). Move `ContainerType` (neutral) to `core`; relocate the referenced input interfaces to `core/types`. -- `libs/element-options.ts` imports concrete `CollectElement`/`ComposableElement` classes — restructure so it depends on `core` interfaces, not concrete element classes (invert via a small interface). - -**Reviewability:** two files; each edge removal is independently checkable. -**Verify:** recipe green. - ---- - -### Task 1.7 — Split `internal-types` (neutral → core) -**Goal:** remove the barrel/`skyflow.ts`/`skyflow-container` upward imports from `internal-types`. -- `internal-types/index.ts` imports the `index-node` barrel, `skyflow.ts`, and `external/skyflow-container` (upward edges). -- Move the neutral internal types (`ElementInfo`, `InternalState`, `Metadata`, `ClientMetadata`, `SkyflowElementProps`, etc.) → `core/types`; break the barrel import by referencing concrete types directly. - -**Reviewability:** type-only moves; surface guard covers regressions. -**Verify:** recipe green. - ---- - -### Task 1.8 — `skyflow-error` base + styles into core -**Goal:** move the neutral error base and style helpers. -- Move `libs/skyflow-error.ts` (the neutral base) → `core/errors`; keep the public `SkyflowError` exported from the package via a re-export from `@core`. -- Move `libs/styles.ts` + neutral parts of `element-options` → `core`. - -**Reviewability:** small; verify the public `SkyflowError` identity is unchanged. -**Verify:** recipe green; `SkyflowError` still exported with identical shape. - ---- - -### Task 1.9 — Extract frame leaf-helpers into core -**Goal:** move the variant-neutral element/frame helpers that both packages will call (the §3.4 leaf helpers). -- Extract into `core` as standalone helpers: the element **validation pass** (`validateElements(options, ctx) → errorMessage`), `getUnformattedValue`, frame/element lookup, checkbox concatenation, duplicate-element detection (`checkForElementMatchRule` / `checkForValueMatch`, currently in `core-utils/collect.ts:471-481`). -- Rewire the privacyDB frame controller + `iframe-form` to call the `@core` helpers. - -**Reviewability:** behavior-preserving extraction; element tests are the guard. -**Verify:** collect/reveal element tests green. - ---- - -### Task 1.10 — Carve `core-utils/collect.ts` & `reveal.ts` (privacyDB) against core -**Goal:** separate neutral request-assembly plumbing from privacyDB transport. -- Move neutral helpers → `core`: `constructElementsInsertReq` (`collect.ts:188`), `formatRecordsForIframe` (`reveal.ts:340`), and any other variant-neutral formatter. -- Leave privacyDB `/v1` builders/parsers in `src` (`constructInsertRecordRequest/Response`, `constructUpdate*`, `insertDataInCollect`, `fetchRecordsByTokenId`, `formatRecordsForClient`, GET/render helpers), now consuming the `@core` helpers. - -**Reviewability:** the two hardest files, but privacyDB-only (no flowDB interleaving on `main`), so it's a clean neutral-vs-transport cut. -**Verify:** recipe green; collect/reveal/detokenize tests green. - ---- - -### Task 1.11 — Pure-JS transport neutral + barrel finalize + boundary hardening -**Goal:** finish the boundary and lock it. -- Ensure the pure-JS **transport** (the `PUREJS_REQUEST` dispatch + container→frame-controller message flow skeleton) is neutral in `core` so future flowvault pure-JS is additive; the `/v1` data layer stays in `src`. -- Finalize `core/index.ts` — the barrel is the surface flowvault will consume in a later phase. -- Flip `import/no-restricted-paths` to **error**. -- Run the full build; **capture the post-Phase-1 bundle-size** (compare to the Task 1.0 baseline, expect ≤ +2%) and confirm coverage ≥ the `main` floor. - -**Reviewability:** mostly wiring + config; final green-field check. -**Verify:** full recipe green; **public surface + telemetry diff = zero**; bundle-size within tolerance; coverage ≥ floor. - ---- - -## Dependency ordering - -``` -1.0 (scaffold) - └─ 1.1 (zero-import leaves) - └─ 1.2 (logs/constants/dom/metrics) - └─ 1.3 (telemetry inject + helpers) - └─ 1.4 (neutral types + base interfaces) ← unblocks the back-edge inversions - ├─ 1.5 (validators) - ├─ 1.6 (helpers/element-options) - └─ 1.7 (internal-types) - └─ 1.8 (error base + styles) - └─ 1.9 (frame leaf-helpers) - └─ 1.10 (collect/reveal carve) - └─ 1.11 (pure-JS transport + finalize) -``` - -Tasks 1.5–1.7 can be parallel PRs once 1.4 lands. Everything else is linear. - -## Risks & watch-items -- **Surface drift:** the emitted-`.d.ts` diff (recipe step 4) is the primary guard — treat any non-empty diff as a defect unless the task intends an identical re-export. -- **Telemetry regression (Task 1.3):** verify `sdk_name_version` explicitly; it's easy to change silently by moving the `package.json` read. -- **`logs-helper → helpers` edge (Task 1.2):** the one ordering hazard in the leaf moves — carry `getSDKLanguageAndVersion` across with it. -- **Circular imports:** relocating interfaces into `core/types` (1.4–1.7) can create cycles if a `core` type pulls a `src` type — the boundary lint rule (error in 1.11) catches these; keep base interfaces dependency-free. -- **Test duplication:** `main` has duplicated `.js`/`.ts` tests — update both (or dedupe) as files move so coverage doesn't drop. diff --git a/docs/phase-2-execution-plan.md b/docs/phase-2-execution-plan.md deleted file mode 100644 index ba114f20..00000000 --- a/docs/phase-2-execution-plan.md +++ /dev/null @@ -1,180 +0,0 @@ -# Phase 2 — Detailed Code Execution Plan (packages + build flowvault) - -Companion to [package-split-plan.md](package-split-plan.md); follows [phase-1-execution-plan.md](phase-1-execution-plan.md). Same model: each task is one PR into the integration branch, individually reviewable, leaving the build + tests green. - ---- - -## Entry state (end of Phase 1) -``` -repo/ - core/ # variant-neutral shared source + @core barrel - src/ # skyflow-js (privacyDB), importing @core - tests/ # skyflow-js tests - tsconfig.base.json # @core/* → core/* - .eslintrc(.js) # import/no-restricted-paths: core/ ⇏ src/ -``` - -## Scope - -Phase 2 does two things: -- **(A) Relocate** the existing tree into `packages/skyflow-js` and stand up npm **workspaces** (skyflow-js behavior-preserving). -- **(B) Build `skyflow-flowvault-js`** (v1.0.0, **elements-only**) from the `2.9.0-beta.1` flowDB deltas, reshaped to **extend `@core`** (base types, error base, leaf helpers) rather than redefine them. - -### Non-goals (deferred) -- **No release-workflow changes** — parameterizing `common-release.yml`, S3/CloudFront, npm publish is **Phase 3**. Phase 2 ends when both packages build all artifacts locally. -- **flowvault stays elements-only** — no pure-JS `Skyflow.*`, no 3DS, no file upload (per §9 decisions). Keep the pure-JS transport neutral in `core/` but do not expose flowvault pure-JS. - -### End state of Phase 2 -``` -repo/ - core/ - packages/ - skyflow-js/ # privacyDB (relocated) — name "skyflow-js" - src/ tests/ package.json tsconfig.json webpack.*.js - skyflow-flowvault-js/ # flowDB (new, v1.0.0) — extends @core - src/ tests/ package.json tsconfig.json webpack.*.js - package.json # private workspace root ("workspaces": ["packages/*"]) - tsconfig.base.json - .eslintrc(.js) # core/ ⇏ packages/ -``` - -**Invariants:** `skyflow-js`'s public surface + telemetry + tests stay **unchanged** throughout. `skyflow-flowvault-js` is anchored to the **ported `*.flowdb.test.js` suites** (its behavioral contract from `2.9.0-beta.1`). - ---- - -## Working model -Same integration branch as Phase 1. One task = one PR, reviewed and merged before the next. `git mv` for all relocations (preserve history). flowvault code is **extracted from the `2.9.0-beta.1` tag** and reshaped onto `@core` — not copied verbatim. - -### Verification recipe (end of every task) -1. `npm install` — workspaces link cleanly. -2. `npm run type-check` (per affected package) — no TS errors. -3. `npm test` (per affected package) — green, no new skips. -4. Build the affected package's artifacts (`build-browser-sdk` / `build-node-sdk` / `build-iframe`). -5. **skyflow-js surface guard:** emitted `types/*.d.ts` diff against the Phase-1-end snapshot = **zero** (relocation must not change the published surface). -6. **flowvault behavior anchor:** the ported `*.flowdb.test.js` suites pass. -7. **Boundary lint:** `import/no-restricted-paths` (`core/` ⇏ `packages/`) passes; and `packages/skyflow-flowvault-js` does **not** import `packages/skyflow-js` (add a zone for that too). -8. **Telemetry snapshot:** skyflow-js → `skyflow-js@`; flowvault → `skyflow-flowvault-js@`, label `JS`. - ---- - -## Group A — Relocate skyflow-js + workspaces - -### Task 2.0 — Workspaces + relocate `skyflow-js` → `packages/skyflow-js` -**Goal:** move the existing package under `packages/` and make the repo a workspace root, with **zero** behavior change. -- Convert the **root `package.json`** into a private workspace root: `"private": true`, `"workspaces": ["packages/*"]`; move the SDK manifest fields (`name` `skyflow-js`, `version`, `main`, `types`, `files`, `scripts`, `dependencies`) into **`packages/skyflow-js/package.json`**. -- `git mv src → packages/skyflow-js/src`, `git mv tests → packages/skyflow-js/tests`, and the SDK's `webpack.*.js` / `jest.config.json` / `tsconfig.json` into the package. -- Fix `@core` resolution for the new depth: keep `tsconfig.base.json` (`baseUrl` at repo root, `@core/* → core/*`); each package `tsconfig.json` `extends` it; update `webpack resolve.alias` → `../../core`; update `jest moduleNameMapper`. -- Root convenience scripts delegate to workspaces (`npm run build -w skyflow-js`, etc.). - -**Reviewability:** large but purely mechanical (moves + path fixes); the surface guard is the safety net. -**Verify:** recipe 1–5; skyflow-js builds all three artifacts; **surface diff = zero**; tests green. - ---- - -## Group B — Build `skyflow-flowvault-js` from `2.9.0-beta.1` as `@core` extensions - -> Each task below **extracts the flowDB slice from the `2.9.0-beta.1` tag** and reshapes it to consume `@core`. flowvault has **no existing consumers**, so "correct" = matches the beta's flowDB behavior, proven by the ported `*.flowdb.test.js` suites. - -### Task 2.1 — Scaffold `packages/skyflow-flowvault-js` -**Goal:** an empty-but-buildable package wired into the workspace. -- `package.json`: `name` `skyflow-flowvault-js`, `version` `1.0.0`, `main`/`types`/`files` mirroring skyflow-js's shape, `dependencies` (same runtime deps). -- `tsconfig.json` extends `tsconfig.base.json`; empty `src/index.ts` (sets `window.SkyflowFlowVault`) + `src/index-node.ts` (empty barrel) + `src/index-internal.ts` (iframe entry stub). -- Register in the boundary lint (flowvault ⇏ skyflow-js). - -**Verify:** `npm install` links it; `type-check` green (no-op package). - -### Task 2.2 — flowDB types extending `@core` bases -**Goal:** the flowDB type surface as **extensions** of the core base interfaces (not redefinitions). -- Port from the beta: the flowDB internal types (`FlowDBInsert*`, `FlowDBUpdate*`, `FlowDBDetokenize*`, `FlowDBRecordResponse`, `FlowDBError`/`FlowDBFullError`, `CollectRecord/Response`, `RevealRecord/Response`, `FlowDBTokenGroupRedaction`) and the public input types (`IFlowDBUpsertOptions`, `IFlowDBRevealElementInput`, `IRevealElementOptions`, `IRevealOptions`, `TokenGroupRedaction`). -- Reshape so element-input/response/upsert types **`extends`** the `@core` base interfaces from Phase 1 Task 1.4. - -**Verify:** `type-check` green; the flowDB response/input shapes match the beta's public contract. - -### Task 2.3 — `skyflow-flowdb-error` extending the `@core` error base -**Goal:** flowvault's error class on top of the neutral base. -- Port `libs/skyflow-flowdb-error.ts` (`SkyflowFlowDBError`, `normalizeFlowDBError`); make `SkyflowFlowDBError` **extend** the `@core` `SkyflowError` base. It remains flowvault's public `SkyflowError` export. - -**Verify:** `type-check`; a small unit test for `normalizeFlowDBError` (snake→camel) passes. - -### Task 2.4 — flowDB **collect** data layer (`/v2`) -**Goal:** flowvault's collect transport, using `@core` assembly helpers. -- Port from beta `core-utils/collect.ts` (flowDB slice): `getFlowDBUpsertForTable`, `constructFlowDBInsertRequest`, `constructFlowDBInsertResponse`, `constructFlowDBInsertError`, `constructFlowDBUpdateRequest`, `flowDBInsertVariant`/`flowDBUpdateVariant`, `executeInsert`, `insertDataInCollectFlowDB`, `updateDataInCollectFlowDB`, and **`replaceCVVTokensInResponse` (cvvMap masking — flowvault-only)**. -- Consume `@core` neutral helpers (`constructElementsInsertReq`, validators) rather than redefining them. - -**Verify:** port `tests/core-utils/collect.flowdb.test.js` → green. - -### Task 2.5 — flowDB **reveal/detokenize** data layer (`/v2`) -**Goal:** flowvault's reveal transport. -- Port from beta `core-utils/reveal.ts` (flowDB slice): `constructFlowDBDetokenizeRequest/Response/Error`, `flowDBDetokenizeVariant`, `executeDetokenize`, `fetchRecordsByTokenIdFlowDB`, `fetchRecordsByTokenIdComposableFlowDB`, `normalizeFlowDBMetadata`, `formatRecordsForClientFlowDB`, `formatRecordsForClientComposableFlowDB`. -- Consume `@core` neutral formatters where they exist. - -**Verify:** port `tests/core-utils/reveal.flowdb.test.js` → green. - -### Task 2.6 — flowvault **collect** path (containers, elements, frame controller) -**Goal:** wire the collect element flow end-to-end for flowDB. -- Bring in the collect container + element + `frame-element-init` + the frame-controller `tokenize()`, adapting skyflow-js's core-based scaffolding: swap in `SkyflowFlowDBError`, the flowDB input/response types, and calls to the flowvault collect data layer (Task 2.4). The `tokenize()` calls `@core` leaf helpers (validate/assemble) then flowvault's request-build + response-parse (§3.4). - -**Verify:** port `frame-element-init.flowdb.test.js` + the frame-controller collect/tokenize flowDB tests → green. - -### Task 2.7 — flowvault **reveal** path (containers, elements, composable) -**Goal:** wire the reveal element flow end-to-end for flowDB. -- Bring in reveal container + element + composable-reveal + `composable-frame-element-init` + the frame `revealData()`, wired to `@core` leaf helpers + the flowvault reveal data layer (Task 2.5) + `SkyflowFlowDBError`. Use `IFlowDBRevealElementInput` (token-only) as the public reveal input. - -**Verify:** port `composable-frame-element-init.flowdb.test.js` + `skyflow-frame-controller.detokenize.flowdb.test.js` → green. - -### Task 2.8 — flowvault `Skyflow` class + iframe entry + public barrels -**Goal:** the package's public shell. -- flowvault `Skyflow` class + `container()` factory (elements-only: COLLECT / COMPOSABLE / REVEAL / COMPOSE_REVEAL; **no** pure-JS methods). -- `index-internal.ts` composes the `@core` iframe skeleton + flowvault's frame controllers (its own iframe build). -- `index.ts` sets `window.SkyflowFlowVault`; `index-node.ts` exports the flowDB public surface with the agreed names (`UpsertOptions`=`IFlowDBUpsertOptions`, `RevealElementInput`=`IFlowDBRevealElementInput`, `CollectResponse/Record`, `RevealResponse/Record`, `RevealOptions`, `SkyflowError`=`SkyflowFlowDBError`, plus the shared enums/classes re-exported from `@core`). - -**Verify:** `type-check`; all agreed flowDB public names are exported with the right shapes. - -### Task 2.9 — flowvault webpack configs + telemetry + iframe URL -**Goal:** produce flowvault's three artifacts with correct identity. -- Per-package `webpack.skyflow-browser.js` / `webpack.skyflow-node.js` / `webpack.iframe.js`, each `merge`-ing the shared `webpack.common.js`; **UMD `library` + IIFE global = `SkyflowFlowVault`**; own `output.path`. -- Telemetry: `DefinePlugin` injects `SDK_NAME=skyflow-flowvault-js` + `SDK_VERSION` from flowvault's `package.json`; **broaden the `@core` language-label check to treat `skyflow-flowvault-js` as `JS`** (leaving the `metaData` wrapper-override → `React` intact). -- flowvault `properties.ts` default iframe URL (`process.env`-based, per §3.2). - -**Verify:** all three flowvault artifacts build; telemetry snapshot = `skyflow-flowvault-js@`, label `JS`; skyflow-js telemetry still `skyflow-js@` (label unchanged). - -### Task 2.10 — flowvault tests, coverage, samples -**Goal:** lock behavior and give consumers examples. -- Land the ported `*.flowdb.test.js` suites under `packages/skyflow-flowvault-js/tests`; add `coverageThreshold` = floor (Phase-1 baseline convention). -- Port the beta's flowDB samples into flowvault samples (script-tag using the `SkyflowFlowVault` global; the `using-typescript` flow against `skyflow-flowvault-js`). -- flowvault `README` stub. - -**Verify:** flowvault coverage ≥ floor; sample type-checks/loads. - -### Task 2.11 — Workspace finalize -**Goal:** both packages build green together; lock the boundary. -- Root scripts: `build`/`test`/`type-check` across `--workspaces`. -- Flip any remaining boundary lint to **error** (core ⇏ packages; flowvault ⇏ skyflow-js). -- Full build of **all six artifacts** (browser/node/iframe × 2 packages); capture flowvault bundle size; confirm skyflow-js size still within the Phase-1 baseline tolerance. - -**Verify:** full recipe green for both packages; skyflow-js surface diff = zero; both telemetry snapshots correct. - ---- - -## Dependency ordering -``` -2.0 (relocate + workspaces) - └─ 2.1 (scaffold flowvault) - └─ 2.2 (flowDB types) - └─ 2.3 (flowDB error base) - ├─ 2.4 (collect data /v2) ── 2.6 (collect path) - └─ 2.5 (reveal data /v2) ─── 2.7 (reveal path) - └─ 2.8 (Skyflow class + entries + barrels) - └─ 2.9 (webpack + telemetry + iframe URL) - └─ 2.10 (tests + coverage + samples) - └─ 2.11 (workspace finalize) -``` -2.4/2.5 parallel after 2.3; 2.6/2.7 parallel after their data layers. - -## Risks & watch-items -- **skyflow-js relocation (2.0):** the risk is `@core`/build-path breakage, not logic. The zero-surface-diff + green build is the gate; keep it one atomic PR so there's no half-moved broken state. -- **Reshape drift (2.2):** the beta redefined types; here they must **extend** `@core` bases while keeping the beta's *public* flowDB shapes. Diff the flowvault `.d.ts` against the beta's documented flowDB contract. -- **cvvMap stays in flowvault (2.4):** it is flowDB-only — never let it leak back into `core/`. -- **Accepted duplication:** flowvault's containers/elements/`Skyflow` class largely mirror skyflow-js's (they differ only by error class, input/response types, and data-layer calls). This is the deliberate loose-coupling trade-off (§3.4) — don't "fix" it by hoisting a template-method controller base into core. -- **Elements-only guard:** ensure no pure-JS `Skyflow.*`, 3DS, or file-upload path is exposed on flowvault (they exist in the beta as internal `#`/commented code — leave them out). -- **Two iframe entries in sync (2.8):** `index-internal.ts` in both packages must stay structurally parallel over the shared `@core` skeleton; keep the entries thin. diff --git a/docs/phase-3-execution-plan.md b/docs/phase-3-execution-plan.md deleted file mode 100644 index edc077e8..00000000 --- a/docs/phase-3-execution-plan.md +++ /dev/null @@ -1,169 +0,0 @@ -# Phase 3 — Detailed Code Execution Plan (build & release) - -Companion to [package-split-plan.md](package-split-plan.md); follows [phase-2-execution-plan.md](phase-2-execution-plan.md). Same model: each task is one PR into the integration branch, individually reviewable. Because these are CI/CD changes, "reviewable" also means **validated in sandbox, never against prod, and skyflow-js's release path is never broken**. - ---- - -## Entry state (end of Phase 2) -``` -repo/ - core/ - packages/skyflow-js/ # privacyDB, builds all 3 artifacts locally - packages/skyflow-flowvault-js/ # flowDB v1.0.0, builds all 3 artifacts locally - package.json # private workspace root -``` -Both packages build locally; **no release automation is package-aware yet.** - -## Scope -Make the release + CI pipeline serve **two independently-versioned packages** from one repo: -- Parameterize `common-release.yml` by package (dir, name, dist path, S3 prefix, publish target, version/tag parsing). -- Per-package **caller workflows** with per-package **tag namespaces** and **secrets** (two iframe deploy pipelines). -- Workspace-aware CI (`pr.yml` / `main.yml`) + `bump_version.sh`. -- Toolchain → Node 18 in the release workflow (CI was bumped in Phase 1). - -### Non-goals -- **No prod cutover in Phase 3.** All validation is in **sandbox**. The integration branch's workflows go live only when the branch merges to `main` (a deliberate, separate cutover step). -- No changes to the SDK code (that's Phases 1–2). - -### End state -- `common-release.yml` is package-parameterized; **skyflow-js releases exactly as before** (defaults preserve current behavior). -- `skyflow-flowvault-js` releases from `flowvault-`-prefixed tags to its own S3 prefix / CloudFront / npm name. -- CI builds + tests both packages. - ---- - -## Working model & golden rules -Same integration branch; one PR per task. For release-workflow tasks: -1. **skyflow-js first, unchanged.** Parameterize with defaults that reproduce today's skyflow-js behavior; prove it in **sandbox** before adding flowvault. -2. **Sandbox before prod, always.** Validate every path against SANDBOX secrets/buckets/distributions. Prod secrets are untouched until the merge-time cutover. -3. **Additive for flowvault.** flowvault gets *new* caller workflows + *new* `FLOWVAULT_*` secrets; nothing skyflow-js depends on is modified in place beyond the shared reusable workflow. - -### Verification recipe (per task) -1. **`actionlint`** (or YAML validation) passes on changed workflows. -2. **CI green** on a PR to the integration branch (for `pr.yml`/`main.yml` tasks). -3. **Sandbox dry-run** (for release tasks): push a throwaway tag in the sandbox env and confirm — correct package version bumped, artifacts at the right **S3 prefix**, iframe reachable at the right **host**, npm published under the right **name/dist-tag**, CloudFront invalidation hit the right **distribution**. -4. **skyflow-js regression guard:** a sandbox skyflow-js release produces the **same** outputs (version scheme, S3 path `v{version}/`, npm name/tag) as before the change. - -### Definition of done (per task) -Recipe green · reviewer approves · prod secrets untouched · skyflow-js sandbox release still correct. - ---- - -## Task list - -### Task 3.0 — Workspace-aware CI (`pr.yml` + `main.yml`), Node 18 -**Goal:** PR/main CI builds and tests **both** packages. -- `actions/setup-node@v4`, Node 18. -- Run `type-check` + `test` + all three builds **per package** (a `matrix: package: [skyflow-js, skyflow-flowvault-js]`, or `npm run