diff --git a/Changelog.md b/Changelog.md index d7a3c9e3..9bed26e1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,9 +1,16 @@ # Changelog ## [Unreleased] +## [3.16.1] - 2026-10-13 +### Changed + +- Added new document type `TaxRegistrationDocument`in kyb widget +- Made all document related field configurable +- Added configurable company fields so businesses are asked to provide only the information selected for verification. +- Made compliance check configurable +- Approve and reject button is always visible ## [3.16.0] - 2026-10-09 -### Added - Added Id service credit calculator ## [3.15.6] - 2026-10-2 diff --git a/src/views/playground/BusinessDetails.vue b/src/views/playground/BusinessDetails.vue index 3b773021..6eee97f8 100644 --- a/src/views/playground/BusinessDetails.vue +++ b/src/views/playground/BusinessDetails.vue @@ -23,7 +23,6 @@
diff --git a/src/views/playground/KybWidgetConfig/Index.vue b/src/views/playground/KybWidgetConfig/Index.vue index f3d35cb9..f377afc3 100644 --- a/src/views/playground/KybWidgetConfig/Index.vue +++ b/src/views/playground/KybWidgetConfig/Index.vue @@ -16,16 +16,19 @@
-
@@ -175,6 +273,21 @@ outline: none; } +.configuration-tab:focus-visible { + border-radius: 4px; + box-shadow: 0 0 0 2px #bfdbfe; +} + +.tab-issue-dot { + background: #dc2626; + border-radius: 50%; + display: inline-block; + height: 6px; + margin-left: 4px; + vertical-align: middle; + width: 6px; +} + .config-card { background-color: #ffffff; border: 1px solid #e5e7eb; @@ -183,6 +296,241 @@ padding: 15px; } +/* Toggle cards (documents and compliance checks) */ +.toggle-card { + align-items: flex-start; + display: flex; + gap: 12px; +} + +.toggle-icon { + align-items: center; + background: #f1f5f9; + border-radius: 8px; + display: inline-flex; + flex: 0 0 auto; + height: 36px; + justify-content: center; + transition: background-color 0.15s ease; + width: 36px; +} + +.toggle-icon >>> .v-icon { + color: #94a3b8 !important; + transition: color 0.15s ease; +} + +.toggle-card.is-enabled .toggle-icon { + background: #eff6ff; +} + +.toggle-card.is-enabled .toggle-icon >>> .v-icon { + color: #2563eb !important; +} + +.toggle-body { + flex: 1; + min-width: 0; +} + +/* Company fields */ +.fields-header { + align-items: flex-start; + display: flex; + gap: 16px; + justify-content: space-between; + margin-bottom: 20px; +} + +.fields-title { + color: #111827; + font-size: 15px; + font-weight: 700; + margin: 0 0 4px; +} + +.fields-subtitle { + color: #64748b; + font-size: 13px; + line-height: 1.5; + margin: 0; + max-width: 640px; +} + +.fields-summary { + background: #f1f5f9; + border-radius: 999px; + color: #475569; + flex: 0 0 auto; + font-size: 12px; + font-weight: 600; + padding: 4px 10px; + white-space: nowrap; +} + +.fields-alert { + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 8px; + color: #991b1b; + display: flex; + font-size: 13px; + gap: 10px; + margin-bottom: 20px; + padding: 12px 14px; +} + +.fields-alert >>> .v-icon { + align-self: flex-start; + color: inherit !important; + margin-top: 1px; +} + +.fields-alert ul { + margin: 4px 0 0; + padding-left: 18px; +} + +.field-group + .field-group { + margin-top: 24px; +} + +.field-group-header { + margin-bottom: 8px; +} + +.field-group-header h6 { + color: #64748b; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.05em; + margin: 0; + text-transform: uppercase; +} + +.field-group-header p { + color: #94a3b8; + font-size: 12px; + margin: 2px 0 0; +} + +.field-list { + border: 1px solid #e5e7eb; + border-radius: 8px; + list-style: none; + margin: 0; + padding: 0; +} + +.field-list.has-issue { + border-color: #fca5a5; +} + +.field-row { + align-items: center; + display: flex; + gap: 16px; + justify-content: space-between; + padding: 12px 16px; +} + +.field-row + .field-row { + border-top: 1px solid #f1f5f9; +} + +.field-info { + display: flex; + flex-direction: column; + min-width: 0; +} + +.field-label { + color: #111827; + font-size: 13px; + font-weight: 600; +} + +.field-description { + color: #64748b; + font-size: 12px; + line-height: 1.45; +} + +.sub-field-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.sub-field-chip { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 4px; + color: #475569; + font-size: 11px; + font-weight: 600; + padding: 1px 6px; +} + +.locked-pill { + align-items: center; + color: #64748b; + display: inline-flex; + flex: 0 0 auto; + font-size: 12px; + font-weight: 600; + gap: 4px; +} + +.locked-pill >>> .v-icon { + color: inherit !important; +} + +.segmented-control { + background: #f1f5f9; + border-radius: 8px; + display: inline-flex; + flex: 0 0 auto; + gap: 2px; + padding: 3px; +} + +.segment { + background: transparent; + border: 0; + border-radius: 6px; + color: #64748b; + cursor: pointer; + font-size: 12px; + font-weight: 600; + min-width: 72px; + padding: 5px 10px; +} + +.segment:hover:not(:disabled):not(.active) { + color: #111827; +} + +.segment:focus { + outline: none; +} + +.segment:focus-visible { + box-shadow: 0 0 0 2px #bfdbfe; +} + +.segment.active { + background: #ffffff; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12); + color: #111827; +} + +.segment-required.active { + color: #1d4ed8; +} + + .sanction-badge-row { display: flex; flex-wrap: wrap; @@ -214,15 +562,17 @@ } @media (max-width: 575.98px) { - .editor-header { + .editor-header, + .fields-header { align-items: stretch; flex-direction: column; } -} -/* Fix for disabled switch opacity */ -:deep(.custom-control-input:disabled ~ .custom-control-label) { - opacity: 0.6; + .field-row { + align-items: flex-start; + flex-direction: column; + gap: 10px; + } } @@ -249,6 +599,35 @@ export default { ...mapGetters('mainStore', ['getAppByAppId']), isContainerShift() { return this.containerShift + }, + alwaysCollectedFields() { + return this.companyFieldDefinitions.filter(field => field.required) + }, + companyFieldRows() { + return this.companyFieldGroups.reduce((rows, group) => rows.concat(group.rows), []) + }, + hasRequiredIdentifier() { + return this.companyFieldGroups + .find(group => group.key === 'identifiers').rows + .some(row => this.companyRowState(row) === 'required') + }, + // Rule violations shown on the Company Details tab and blocking save + companyFieldIssues() { + const issues = this.companyFieldRows + .filter(row => this.isCompanyRowRequiredByDocument(row) && this.companyRowState(row) !== 'required') + .map(row => `${row.label} must be set to Required while you collect the ${row.documentLabel}. Change it here or turn off the document in Document Collection.`) + if (!this.hasRequiredIdentifier) { + issues.push('Require at least one identifier: registration number or tax ID.') + } + return issues + }, + companyFieldSummary() { + const states = this.companyFieldRows.map(row => this.companyRowState(row)) + const always = this.alwaysCollectedFields.length + return { + collected: always + states.filter(state => state !== 'off').length, + required: always + states.filter(state => state === 'required').length + } } }, async mounted() { @@ -262,6 +641,7 @@ export default { ...this.kybWidgetConfigTemp, ...this.kybWidgetConfig } + this.normalizeCompanyFields() } this.appId = this.$route.params.appId; @@ -292,6 +672,10 @@ export default { label: "Collect Certificate of Incorporation", description: 'Enable collection of company incorporation certificate for business verification.' }, + collectTaxRegistrationDoc: { + label: "Collect Tax Registration Document", + description: 'Enable collection of the company’s tax registration certificate or equivalent tax document.' + }, collectPowerOfAttorneyDoc: { label: "Collect Power of Attorney Document", description: 'Enable collection of power of attorney documentation for business representative verification.' @@ -300,6 +684,7 @@ export default { label: "Collect Address Proof Document", description: 'Enable collection of business address verification documents.' }, + collectZkProof: { label: "Enable Zero Knowledge Proof", description: 'Enable businesses to share only proof of their data for enhanced privacy and compliance.' @@ -352,7 +737,64 @@ export default { accessDenied: false, accessDeniedMsg: '', activeTab: 'Branding', - tabs: ['Branding', 'Document Collection', 'Compliance Checks'], + tabs: ['Branding', 'Document Collection', 'Company Details', 'Compliance Checks'], + companyFieldDefinitions: [ + { fieldName: 'name', label: 'Company Name', description: 'Registered or legal name of the company.', type: 'text', required: true }, + { fieldName: 'domain', label: 'Company Domain', description: 'Website domain associated with the company.', type: 'text', required: true }, + { fieldName: 'countryOfRegistration', label: 'Country of Registration', description: 'Country where the company is registered.', type: 'country', required: true }, + { fieldName: 'region', label: 'Region', description: 'Region where the company operates. This can differ from its country of registration.', type: 'text', required: true }, + { fieldName: 'registrationNumberType', label: 'Registration Number Type', description: 'Type of company registration number.', type: 'registration-type' }, + { fieldName: 'registrationNumber', label: 'Registration Number', description: 'Company registration or incorporation number.', type: 'text' }, + { fieldName: 'taxIdType', label: 'Tax ID Type', description: 'Type of company tax identifier.', type: 'tax-id-type' }, + { fieldName: 'taxIdNumber', label: 'Tax ID Number', description: 'Company tax identification number.', type: 'text' }, + { fieldName: 'address', label: 'Company Address', description: 'Registered or operating business address.', type: 'address' } + ], + // Optional fields as the editor shows them. Paired fields (number + type) share one setting. + // The backend requires a row tied to a document to be required while that document is collected. + companyFieldGroups: [ + { + key: 'identifiers', + title: 'Identifiers', + hint: 'Require at least one so each business can be matched against official records.', + rows: [ + { + key: 'registration', + label: 'Registration Number', + description: 'Company registration or incorporation number, along with its type.', + fieldNames: ['registrationNumber', 'registrationNumberType'], + documentKey: 'collectCertOfIncorporationDoc', + documentLabel: 'Certificate of Incorporation' + }, + { + key: 'tax', + label: 'Tax ID', + description: 'Company tax identification number, along with its type (for example GSTIN or VAT).', + fieldNames: ['taxIdNumber', 'taxIdType'], + documentKey: 'collectTaxRegistrationDoc', + documentLabel: 'Tax Registration Document' + } + ] + }, + { + key: 'address', + title: 'Registered Address', + hint: '', + rows: [ + { + key: 'address', + label: 'Registered address', + description: 'The company\'s registered business address. Businesses fill in these fields:', + fieldNames: ['address'], + subFields: ['Street', 'City', 'Province / State', 'Postal code', 'Country'] + } + ] + } + ], + companyFieldStateOptions: [ + { value: 'off', label: 'Off' }, + { value: 'optional', label: 'Optional' }, + { value: 'required', label: 'Required' } + ], appId: "", app: {}, kybWidgetConfigTemp: { @@ -361,6 +803,15 @@ export default { collectCertOfIncorporationDoc: true, collectPowerOfAttorneyDoc: true, collectAddressProofDoc: true, + collectTaxRegistrationDoc: false, + companyFields: [ + { fieldName: 'name', type: 'text', isMandatory: true }, + { fieldName: 'domain', type: 'text', isMandatory: true }, + { fieldName: 'region', type: 'text', isMandatory: true }, + { fieldName: 'countryOfRegistration', type: 'country', isMandatory: true }, + { fieldName: 'registrationNumber', type: 'text', isMandatory: true }, + { fieldName: 'registrationNumberType', type: 'registration-type', isMandatory: true } + ], collectZkProof: { enable: false, proofType: "", @@ -393,6 +844,67 @@ export default { ...mapMutations('mainStore', ['setKybWidgetConfig']), ...mapActions('mainStore', ['createAppsKybWidgetConfig', 'fetchAppsKybWidgetConfig', 'updateAppsKybWidgetConfig']), + normalizeCompanyFields() { + // Older KYB widget configs predate companyFields. An empty array is also + // treated as legacy data because the editor requires the core company + // fields and otherwise presents an unusable all-off configuration. + if (!Array.isArray(this.kybWidgetConfigTemp.companyFields) || this.kybWidgetConfigTemp.companyFields.length === 0) { + this.kybWidgetConfigTemp.companyFields = [ + { fieldName: 'name', type: 'text', isMandatory: true }, + { fieldName: 'domain', type: 'text', isMandatory: true }, + { fieldName: 'region', type: 'text', isMandatory: true }, + { fieldName: 'countryOfRegistration', type: 'country', isMandatory: true }, + { fieldName: 'registrationNumber', type: 'text', isMandatory: true }, + { fieldName: 'registrationNumberType', type: 'registration-type', isMandatory: true } + ] + return + } + + // Always-collected fields must be present and required + let fields = this.kybWidgetConfigTemp.companyFields.filter(field => !this.alwaysCollectedFields.some(def => def.fieldName === field.fieldName)) + const always = this.alwaysCollectedFields.map(def => ({ fieldName: def.fieldName, type: def.type, isMandatory: true })) + + // Paired fields share one setting; take it from whichever half was saved + this.companyFieldRows.filter(row => row.fieldNames.length > 1).forEach(row => { + const saved = row.fieldNames.map(name => fields.find(field => field.fieldName === name)).find(Boolean) + fields = fields.filter(field => !row.fieldNames.includes(field.fieldName)) + if (saved) fields.push(...this.buildRowFields(row, saved.isMandatory)) + }) + + this.kybWidgetConfigTemp.companyFields = always.concat(fields) + }, + + buildRowFields(row, isMandatory) { + return row.fieldNames.map(name => { + const definition = this.companyFieldDefinitions.find(field => field.fieldName === name) + return { fieldName: name, type: definition.type, isMandatory: Boolean(isMandatory) } + }) + }, + + // 'off' | 'optional' | 'required' + companyRowState(row) { + const field = (this.kybWidgetConfigTemp.companyFields || []).find(item => item.fieldName === row.fieldNames[0]) + if (!field) return 'off' + return field.isMandatory ? 'required' : 'optional' + }, + + isCompanyRowRequiredByDocument(row) { + return Boolean(row.documentKey && this.kybWidgetConfigTemp[row.documentKey]) + }, + + setCompanyRowState(row, state) { + const fields = (this.kybWidgetConfigTemp.companyFields || []).filter(field => !row.fieldNames.includes(field.fieldName)) + if (state !== 'off') fields.push(...this.buildRowFields(row, state === 'required')) + this.kybWidgetConfigTemp.companyFields = fields + }, + + // Collecting a document needs its identifier, so turning one on makes that row required + onIdentifierDocumentToggle(documentKey, enabled) { + if (!enabled) return + const row = this.companyFieldRows.find(item => item.documentKey === documentKey) + if (row) this.setCompanyRowState(row, 'required') + }, + handleApiError(error, method = 'GET') { const message = typeof error === 'string' ? error : error?.message || 'Something went wrong'; if (method.toUpperCase() === 'GET' && isAccessDeniedError(error)) { @@ -409,6 +921,17 @@ export default { throw new Error('Issuer DID is required') } + if (this.companyFieldIssues.length) { + this.activeTab = 'Company Details' + throw new Error(this.companyFieldIssues[0]) + } + + if (!this.kybWidgetConfigTemp.collectCertOfIncorporationDoc && + !this.kybWidgetConfigTemp.collectAddressProofDoc && + !this.kybWidgetConfigTemp.collectTaxRegistrationDoc) { + throw new Error('Enable at least one company document: Certificate of Incorporation, Address Proof, or Tax Registration Document') + } + if (this.kybWidgetConfigTemp.collectZkProof.enable) { if (!this.kybWidgetConfigTemp.collectZkProof.proofType) { throw new Error('ZK Proof type is required when ZK Proof is enabled') @@ -424,6 +947,7 @@ export default { async saveConfiguration() { try { this.isLoading = true; + this.normalizeCompanyFields() this.validateField() this.setKybWidgetConfig(this.kybWidgetConfigTemp) await this.createAppsKybWidgetConfig() @@ -440,6 +964,7 @@ export default { async updateConfiguration() { try { this.isLoading = true; + this.normalizeCompanyFields() this.validateField() this.setKybWidgetConfig(this.kybWidgetConfigTemp) await this.updateAppsKybWidgetConfig()