@@ -421,6 +744,10 @@ import { isAccessDeniedError } from '../../../utils/accessDenied';
import MarketplaceList from '../../../components/MarketplaceList.vue';
import HFBeta from '../../../components/element/HFBeta.vue';
import config from '../../../config';
+import countries from 'i18n-iso-countries';
+import enLocale from 'i18n-iso-countries/langs/en.json';
+
+countries.registerLocale(enLocale);
export default {
name: "WidgetConfig",
@@ -495,7 +822,7 @@ export default {
},
immediate: true
- }
+ },
},
computed: {
...mapState({
@@ -523,6 +850,39 @@ export default {
});
return options
},
+ countryOptions() {
+ return Object.entries(countries.getNames('en', { select: 'alias' }))
+ .map(([alpha2, name]) => {
+ const alpha3 = countries.alpha2ToAlpha3(alpha2)
+ const displayName = alpha2 === 'AE' ? 'United Arab Emirates' : name
+
+ return {
+ value: alpha3,
+ text: displayName,
+ displayText: `${displayName} (${alpha3})`,
+ flag: this.countryFlagFromAlpha2(alpha2),
+ }
+ })
+ .filter(country => !!country.value)
+ .sort((a, b) => a.text.localeCompare(b.text))
+ },
+ selectedJurisdictionCountries() {
+ const selected = this.widgetConfigTemp.jurisdictionRules?.countries || []
+ return selected.map(code => {
+ const option = this.countryOptions.find(country => country.value === code)
+ return option || {
+ value: code,
+ text: code,
+ displayText: code,
+ flag: this.countryFlagFromAlpha3(code),
+ }
+ })
+ },
+ selectedJurisdictionActionDescription() {
+ const action = this.widgetConfigTemp.jurisdictionRules?.actionOnRestriction
+ const option = this.jurisdictionActionOptions.find(option => option.value === action)
+ return option?.description || ''
+ },
},
async mounted() {
@@ -550,6 +910,7 @@ export default {
if (typeof this.widgetConfigTemp.isMobileAssistedVerification !== 'boolean') {
this.$set(this.widgetConfigTemp, 'isMobileAssistedVerification', true)
}
+ this.ensureJurisdictionRules()
this.trustedIssuersList = [...this.getMarketPlaceApps];
this.appId = this.$route.params.appId;
@@ -659,6 +1020,11 @@ export default {
label: "Mobile-Assisted Verification",
description: "When enabled, users who start verification on a desktop will continue ID verification on their mobile device by scanning a QR code. This improves verification success rates, provides a better user experience, and strengthens proof of possession. Disable to allow the entire verification process to be completed on the desktop."
},
+ jurisdictionRules: {
+ label: "Enable Jurisdictional Restrictions",
+ description: "Enforce compliance policies by restricting access based on document issuing country and nationality.",
+ secondaryDescription: "Choose to either block high-risk regions (Blocklist) or restrict onboarding to specific permitted territories (Allowlist)."
+ },
},
fullPage: true,
isLoading: false,
@@ -698,6 +1064,12 @@ export default {
isVaultEnabled: true,
isEmailNotificationEnabled: true,
isMobileAssistedVerification: true,
+ jurisdictionRules: {
+ enabled: false,
+ strategy: 'BLOCKLIST',
+ countries: [],
+ actionOnRestriction: 'HARD_REJECT',
+ },
issuerDID: "",
issuerVerificationMethodId: "",
},
@@ -726,6 +1098,30 @@ export default {
text: "Government ID"
},
],
+ jurisdictionStrategyOptions: [
+ {
+ value: 'BLOCKLIST',
+ text: 'Blocklist',
+ description: 'Block selected countries',
+ },
+ {
+ value: 'ALLOWLIST',
+ text: 'Allowlist',
+ description: 'Only permit selected countries',
+ },
+ ],
+ jurisdictionActionOptions: [
+ {
+ value: 'HARD_REJECT',
+ text: 'Hard Reject (Stop Onboarding)',
+ description: 'Rejects the session immediately if the user matches the configured jurisdiction rule.',
+ },
+ {
+ value: 'MANUAL_REVIEW',
+ text: 'Manual Review',
+ description: 'Allows the user to complete verification, then sends the session to the manual review queue.',
+ },
+ ],
}
},
@@ -742,6 +1138,84 @@ export default {
this.notifyErr(message)
},
+ defaultJurisdictionRules() {
+ return {
+ enabled: false,
+ strategy: 'BLOCKLIST',
+ countries: [],
+ actionOnRestriction: 'HARD_REJECT',
+ }
+ },
+ ensureJurisdictionRules() {
+ const defaults = this.defaultJurisdictionRules()
+
+ if (!this.widgetConfigTemp.jurisdictionRules) {
+ this.$set(this.widgetConfigTemp, 'jurisdictionRules', defaults)
+ return
+ }
+
+ const existingRules = this.widgetConfigTemp.jurisdictionRules
+ Object.keys(defaults).forEach(key => {
+ if (!Object.prototype.hasOwnProperty.call(existingRules, key)) {
+ this.$set(existingRules, key, defaults[key])
+ }
+ })
+
+ existingRules.enabled = existingRules.enabled === true
+ existingRules.strategy = ['BLOCKLIST', 'ALLOWLIST'].includes(existingRules.strategy)
+ ? existingRules.strategy
+ : defaults.strategy
+ existingRules.countries = Array.isArray(existingRules.countries)
+ ? existingRules.countries.map(country => String(country).toUpperCase()).filter(country => !!country)
+ : []
+ existingRules.actionOnRestriction = ['HARD_REJECT', 'MANUAL_REVIEW'].includes(existingRules.actionOnRestriction)
+ ? existingRules.actionOnRestriction
+ : defaults.actionOnRestriction
+ },
+ countryFlagFromAlpha2(alpha2) {
+ if (!alpha2 || alpha2.length !== 2) return ''
+ return alpha2
+ .toUpperCase()
+ .replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt()))
+ },
+ countryFlagFromAlpha3(alpha3) {
+ const alpha2 = countries.alpha3ToAlpha2(alpha3)
+ return this.countryFlagFromAlpha2(alpha2)
+ },
+ removeJurisdictionCountry(countryCode) {
+ const selectedCountries = this.widgetConfigTemp.jurisdictionRules.countries || []
+ this.widgetConfigTemp.jurisdictionRules.countries = selectedCountries.filter(country => country !== countryCode)
+ },
+ isJurisdictionCountrySelected(countryCode) {
+ return (this.widgetConfigTemp.jurisdictionRules.countries || []).includes(countryCode)
+ },
+ validateJurisdictionRules() {
+ this.ensureJurisdictionRules()
+ const rules = this.widgetConfigTemp.jurisdictionRules
+
+ if (!rules.enabled) {
+ rules.countries = []
+ rules.actionOnRestriction = rules.actionOnRestriction || 'HARD_REJECT'
+ return
+ }
+
+ if (!['BLOCKLIST', 'ALLOWLIST'].includes(rules.strategy)) {
+ throw new Error('Kindly select a jurisdiction enforcement strategy')
+ }
+
+ if (!Array.isArray(rules.countries) || rules.countries.length === 0) {
+ throw new Error('Kindly select at least one target country for jurisdiction restrictions')
+ }
+
+ const invalidCountries = rules.countries.filter(country => !/^[A-Z]{3}$/.test(country) || !countries.alpha3ToAlpha2(country))
+ if (invalidCountries.length > 0) {
+ throw new Error(`Invalid country code(s): ${invalidCountries.join(', ')}`)
+ }
+
+ if (!['HARD_REJECT', 'MANUAL_REVIEW'].includes(rules.actionOnRestriction)) {
+ throw new Error('Kindly select a jurisdiction action on match')
+ }
+ },
selectedServiceEventHandler(event) {
// Guard: ignore entries with no issuerDid
@@ -865,6 +1339,7 @@ export default {
this.widgetConfigTemp.isWidgetLogin = this.widgetConfigTemp.isWidgetLogin !== false
this.widgetConfigTemp.isMobileAssistedVerification = this.widgetConfigTemp.isMobileAssistedVerification !== false
this.migratedocumentUploadMode()
+ this.validateJurisdictionRules()
if (!this.widgetConfigTemp.issuerDID) {
throw new Error('Issuer DID is required')
}
@@ -924,6 +1399,7 @@ export default {
await this.createAppsWidgetConfig()
if (this.widgetConfig) {
this.widgetConfigTemp = JSON.parse(JSON.stringify(this.widgetConfig))
+ this.ensureJurisdictionRules()
}
this.isLoading = false
@@ -947,6 +1423,7 @@ export default {
if (this.widgetConfig) {
// this.widgetConfigTemp = { ...this.widgetConfig }
this.widgetConfigTemp.trustedIssuer = this.widgetConfigTemp.issuerDID ? true : false;
+ this.ensureJurisdictionRules()
}
this.isLoading = false
} catch (e) {