From 96390041a6ea10c63664bbcd61afc159a9d91dcb Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:40:50 +0530 Subject: [PATCH 01/19] Revert "fix: ecdsa (#1625)" (#1671) This reverts commit 13d81c53bfb9b2514c178e59f7dec0e9b6cd1d24. --- .../crypto/bigInt/bigIntComparators.circom | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/circuits/circuits/utils/crypto/bigInt/bigIntComparators.circom b/circuits/circuits/utils/crypto/bigInt/bigIntComparators.circom index a90dafa26..0d6c53003 100644 --- a/circuits/circuits/utils/crypto/bigInt/bigIntComparators.circom +++ b/circuits/circuits/utils/crypto/bigInt/bigIntComparators.circom @@ -17,13 +17,13 @@ include "../utils/switcher.circom"; // Can check for 2 bigints equality if in is sub of each chunk of those numbers template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) { assert(CHUNK_NUMBER >= 2); - + var EPSILON = 3; - + assert(MAX_CHUNK_SIZE + EPSILON <= 253); - + signal input in[CHUNK_NUMBER]; - + signal carry[CHUNK_NUMBER - 1]; component carryRangeChecks[CHUNK_NUMBER - 1]; for (var i = 0; i < CHUNK_NUMBER - 1; i++){ @@ -45,9 +45,9 @@ template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) { // Works with overflowed signed chunks // To handle megative values we use sign // Sign is var and can be changed, but it should be a problem -// Sign change means that we can calculate for -in instead of in, +// Sign change means that we can calculate for -in instead of in, // But if in % p == 0 means that -in % p == 0 too, so no exploit here -// Problem lies in other one: +// Problem lies in other one: // k - is result of div func, and can be anything (var) // we check k * p - in === 0 // k * p is result of big multiplication @@ -71,9 +71,9 @@ template BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER) { template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NUMBER, CHUNK_NUMBER_MODULUS){ signal input in[CHUNK_NUMBER]; signal input modulus[CHUNK_NUMBER_MODULUS]; - + var CHUNK_NUMBER_DIV = MAX_CHUNK_NUMBER - CHUNK_NUMBER_MODULUS + 1; - + var reduced[200] = reduce_overflow_signed_dl(CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NUMBER, MAX_CHUNK_SIZE, in); var div_result[2][200] = long_div_dl(CHUNK_SIZE, CHUNK_NUMBER_MODULUS, CHUNK_NUMBER_DIV - 1, reduced, modulus); signal sign <-- reduced[199]; @@ -88,7 +88,7 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU for (var i = 0; i < CHUNK_NUMBER_DIV; i++){ k[i] <-- div_result[0][i]; kRangeChecks[i] = Num2Bits(CHUNK_SIZE); - kRangeChecks[i].in <== k[i]; + kRangeChecks[i].in <-- k[i]; } component mult; @@ -101,7 +101,7 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU mult.in1 <== modulus; mult.in2 <== k; } - + component swicher[CHUNK_NUMBER]; component isZero = BigIntIsZero(CHUNK_SIZE, MAX_CHUNK_SIZE, MAX_CHUNK_NUMBER); @@ -116,5 +116,5 @@ template BigIntIsZeroModP(CHUNK_SIZE, MAX_CHUNK_SIZE, CHUNK_NUMBER, MAX_CHUNK_NU for (var i = CHUNK_NUMBER; i < MAX_CHUNK_NUMBER; i++){ isZero.in[i] <== mult.out[i]; } - -} + +} \ No newline at end of file From cae937b8ba41490d29787c800cc930cd687271a9 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:48:12 +0530 Subject: [PATCH 02/19] feat: add register_kyc and vc_and_disclose_kyc verifiers (#1672) --- .../disclose/Verifier_vc_and_disclose_kyc.sol | 364 ++++++++++++++++++ .../register_kyc/Verifier_register_kyc.sol | 189 +++++++++ 2 files changed, 553 insertions(+) create mode 100644 contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol create mode 100644 contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol diff --git a/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol b/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol new file mode 100644 index 000000000..b8de45e2c --- /dev/null +++ b/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Groth16Verifier { + // Scalar field size + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // Base field size + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + // Verification Key data + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + uint256 constant deltax1 = 1022502948747070596300631872305350196366208813582081229292413330002410493735; + uint256 constant deltax2 = 8307404806875602039009979465400882149520343934575147532878670270259674144681; + uint256 constant deltay1 = 8725996148009629609617423651062395041554350094385944632997372312828608644955; + uint256 constant deltay2 = 19505227144542990355285832777856832082655385455315296491381347497982380087331; + + + uint256 constant IC0x = 16649376790350306128495410672000438222835355361873864679185308928608342391377; + uint256 constant IC0y = 1365830659239397567654193478106544803466926587095831397836882385286292210457; + + uint256 constant IC1x = 12768368041823022971486465099843313755353560181066686496309262693573983752166; + uint256 constant IC1y = 8959643464054312389755875312066576344157543684040889350558798123653714759323; + + uint256 constant IC2x = 8026951355325092256379108005740615512895662065129471323964253392093201472413; + uint256 constant IC2y = 17729685419344675830181571225504519401370157618831493299320871505193568194542; + + uint256 constant IC3x = 13865750614916211164740113517816425481179265306761612472818567385469595810190; + uint256 constant IC3y = 6210007189067774389269573600168370223250403017805496113623335642264819992738; + + uint256 constant IC4x = 16855313964021865460083277912281502340407051430688518561820294646056966683723; + uint256 constant IC4y = 15265407205922489364865678414919162208795257265772110915033785419192236363960; + + uint256 constant IC5x = 18598823774356508040525215881560556738983729535652356395586704599152692518280; + uint256 constant IC5y = 18145817576163407281749708126167770321482159783050035647989919114769433256079; + + uint256 constant IC6x = 7929686493832109041041190086485345905029205802382475316611421597823511641043; + uint256 constant IC6y = 19169046602940406351907027759303697432610627026407453208752335429425017694574; + + uint256 constant IC7x = 2605668546149689485076733864456601989800612639397730351435615085329568572059; + uint256 constant IC7y = 2242419572125099587271391127551951332349827207830958146376081280864531825864; + + uint256 constant IC8x = 17230061988111645534990582267868011734783232047326494254312685097544413153459; + uint256 constant IC8y = 10806577457667861555253433417098515955632524053970338643826272138544403320442; + + uint256 constant IC9x = 3751984630395628299497200107740113530312143585224331604497180428031979981854; + uint256 constant IC9y = 15676455188720477849218254715359881022685281346012746362600653176819367175994; + + uint256 constant IC10x = 9038868170600703467507268624782850799834426621476374278712452873055805013104; + uint256 constant IC10y = 9698587198888135369066906249654396893723648003242241945599284193157738042248; + + uint256 constant IC11x = 6050467884563375668249040797272149300003806466909114026944043296882360309360; + uint256 constant IC11y = 15900287959991498727296171595521639394049115178198151794906977584504380285297; + + uint256 constant IC12x = 11084322708760789175416300406920316493444723572225966905156819463716045081320; + uint256 constant IC12y = 11218515196222567596688687943809578734267033209068034707100619316839921252394; + + uint256 constant IC13x = 10645041863169277188776881369692412104739148582039109401067090622235062084156; + uint256 constant IC13y = 5266268354502390834581900591132009542571872858584466937449333517597831148030; + + uint256 constant IC14x = 12641747272597271663246870871466152965248117816492334493753291231347523232168; + uint256 constant IC14y = 19526003775802419962730302158408658198175393685733749794278416969198861577034; + + uint256 constant IC15x = 6139284918750361257008863566645097867991292622068199456332000872393801256773; + uint256 constant IC15y = 7099084867504428315337895159166860608559331005995192184490932820607010680845; + + uint256 constant IC16x = 9370432203154443644773178040475615441452364961035990256255996609230750218064; + uint256 constant IC16y = 17951757691776403072537537626795200133221243393670030429694486485017127221358; + + uint256 constant IC17x = 21581607541319264321515681298226106781535771321110191776762670932817827595844; + uint256 constant IC17y = 7631049069535860061742036261740730390300464507981117501570404056719958498930; + + uint256 constant IC18x = 16588935529361800732448688229721305142336631834288163321894359880448688608191; + uint256 constant IC18y = 4976649298929967469596409013742801233623738930274577396507275281714439091100; + + uint256 constant IC19x = 13336088316263130029440976636885322206279122461816212975585641922353453096719; + uint256 constant IC19y = 668527371723708514830022396101506352277923231593513590339198147917179128262; + + uint256 constant IC20x = 7911418535344866382682474453536883970529338904273675929069409842800763592456; + uint256 constant IC20y = 6722145715621557485364045815849938484983110008747946723738151730812429418202; + + uint256 constant IC21x = 610873214241184085635414594211441831430912772471117234461302269567691174096; + uint256 constant IC21y = 16969907768023728182903317862310886370963194429698287724301462949165910596854; + + uint256 constant IC22x = 659738555556673077218073955988504765951032248025470001896149485964044510568; + uint256 constant IC22y = 2124464077179769137643014583429957482256390408775774347541901875987080182668; + + uint256 constant IC23x = 11040330531093768074742977048495269267038172161278331102262692904222746927915; + uint256 constant IC23y = 20387648111599243028561521301140310714164415003338654058061856932087967245514; + + uint256 constant IC24x = 6937058621269922207815167233155518898032328662416059831807664411944661190679; + uint256 constant IC24y = 3779340684837021741207549471402298796167963069596080462551336236827030143602; + + uint256 constant IC25x = 20956067714892758188531163534075112952656779768842660715243328162174316184647; + uint256 constant IC25y = 9697689335367034906644638465039998846629732846527791686651080885302279721947; + + uint256 constant IC26x = 10803066158517027587330447158982829324243112587050865062666733319696533170000; + uint256 constant IC26y = 16966880529095588436103115659246637747363575619917237189424029126730846465979; + + uint256 constant IC27x = 12430600018955874842029331801839308658974272583893366935707885910189427842476; + uint256 constant IC27y = 14602780957678176966948503351865628319039612308733335242961008886115024541985; + + uint256 constant IC28x = 10923748125791784887614451982072899321420747436037959145471646494829305705731; + uint256 constant IC28y = 6050274667868774010280923182747429242888928748472706014853484883020658961073; + + uint256 constant IC29x = 1170885743391113947515531032472753161485583617637753865725092942330476093342; + uint256 constant IC29y = 19204742121781488340297839383055704899252648836617466985181418250802660585322; + + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[29] calldata _pubSignals) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, r)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32))) + + g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64))) + + g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96))) + + g1_mulAccC(_pVk, IC5x, IC5y, calldataload(add(pubSignals, 128))) + + g1_mulAccC(_pVk, IC6x, IC6y, calldataload(add(pubSignals, 160))) + + g1_mulAccC(_pVk, IC7x, IC7y, calldataload(add(pubSignals, 192))) + + g1_mulAccC(_pVk, IC8x, IC8y, calldataload(add(pubSignals, 224))) + + g1_mulAccC(_pVk, IC9x, IC9y, calldataload(add(pubSignals, 256))) + + g1_mulAccC(_pVk, IC10x, IC10y, calldataload(add(pubSignals, 288))) + + g1_mulAccC(_pVk, IC11x, IC11y, calldataload(add(pubSignals, 320))) + + g1_mulAccC(_pVk, IC12x, IC12y, calldataload(add(pubSignals, 352))) + + g1_mulAccC(_pVk, IC13x, IC13y, calldataload(add(pubSignals, 384))) + + g1_mulAccC(_pVk, IC14x, IC14y, calldataload(add(pubSignals, 416))) + + g1_mulAccC(_pVk, IC15x, IC15y, calldataload(add(pubSignals, 448))) + + g1_mulAccC(_pVk, IC16x, IC16y, calldataload(add(pubSignals, 480))) + + g1_mulAccC(_pVk, IC17x, IC17y, calldataload(add(pubSignals, 512))) + + g1_mulAccC(_pVk, IC18x, IC18y, calldataload(add(pubSignals, 544))) + + g1_mulAccC(_pVk, IC19x, IC19y, calldataload(add(pubSignals, 576))) + + g1_mulAccC(_pVk, IC20x, IC20y, calldataload(add(pubSignals, 608))) + + g1_mulAccC(_pVk, IC21x, IC21y, calldataload(add(pubSignals, 640))) + + g1_mulAccC(_pVk, IC22x, IC22y, calldataload(add(pubSignals, 672))) + + g1_mulAccC(_pVk, IC23x, IC23y, calldataload(add(pubSignals, 704))) + + g1_mulAccC(_pVk, IC24x, IC24y, calldataload(add(pubSignals, 736))) + + g1_mulAccC(_pVk, IC25x, IC25y, calldataload(add(pubSignals, 768))) + + g1_mulAccC(_pVk, IC26x, IC26y, calldataload(add(pubSignals, 800))) + + g1_mulAccC(_pVk, IC27x, IC27y, calldataload(add(pubSignals, 832))) + + g1_mulAccC(_pVk, IC28x, IC28y, calldataload(add(pubSignals, 864))) + + g1_mulAccC(_pVk, IC29x, IC29y, calldataload(add(pubSignals, 896))) + + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + checkField(calldataload(add(_pubSignals, 32))) + + checkField(calldataload(add(_pubSignals, 64))) + + checkField(calldataload(add(_pubSignals, 96))) + + checkField(calldataload(add(_pubSignals, 128))) + + checkField(calldataload(add(_pubSignals, 160))) + + checkField(calldataload(add(_pubSignals, 192))) + + checkField(calldataload(add(_pubSignals, 224))) + + checkField(calldataload(add(_pubSignals, 256))) + + checkField(calldataload(add(_pubSignals, 288))) + + checkField(calldataload(add(_pubSignals, 320))) + + checkField(calldataload(add(_pubSignals, 352))) + + checkField(calldataload(add(_pubSignals, 384))) + + checkField(calldataload(add(_pubSignals, 416))) + + checkField(calldataload(add(_pubSignals, 448))) + + checkField(calldataload(add(_pubSignals, 480))) + + checkField(calldataload(add(_pubSignals, 512))) + + checkField(calldataload(add(_pubSignals, 544))) + + checkField(calldataload(add(_pubSignals, 576))) + + checkField(calldataload(add(_pubSignals, 608))) + + checkField(calldataload(add(_pubSignals, 640))) + + checkField(calldataload(add(_pubSignals, 672))) + + checkField(calldataload(add(_pubSignals, 704))) + + checkField(calldataload(add(_pubSignals, 736))) + + checkField(calldataload(add(_pubSignals, 768))) + + checkField(calldataload(add(_pubSignals, 800))) + + checkField(calldataload(add(_pubSignals, 832))) + + checkField(calldataload(add(_pubSignals, 864))) + + checkField(calldataload(add(_pubSignals, 896))) + + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } + } diff --git a/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol b/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol new file mode 100644 index 000000000..263f6efd9 --- /dev/null +++ b/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Verifier_register_kyc { + // Scalar field size + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // Base field size + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + // Verification Key data + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + uint256 constant deltax1 = 5096083179356499711134631633887324869705417987781707067448982643113793288629; + uint256 constant deltax2 = 21697837263794337150638011065730493662458737594964062811076864693347158601584; + uint256 constant deltay1 = 10401404284625717188368140886450294801087446278285114268746933223843924747393; + uint256 constant deltay2 = 21623976071772575613470418289568781837131470676146510317928308200173145329920; + + + uint256 constant IC0x = 3168135977548073774669686196671110956985263260631963004209946350111009871783; + uint256 constant IC0y = 19251271161827058925074199219712324559154387560340229136388386911360884273664; + + uint256 constant IC1x = 10113211405751296270501192543847397464767605934439509015058826831045146327835; + uint256 constant IC1y = 20906232714001423808044993672348326367907746369031125809295685889757083482955; + + uint256 constant IC2x = 6698755477482983343149024614634334433817620579582112164753380215391423709716; + uint256 constant IC2y = 19611748192038263311129103965451949878445716642076010695268731681711285170849; + + uint256 constant IC3x = 14337814476916517064830141950947112575746971807933737544800387322677759596630; + uint256 constant IC3y = 20134363192770038065525691357184427373049635942597185153353604022941231384818; + + uint256 constant IC4x = 11598465374717791235735036209864180918816853983932860910077820062417244512066; + uint256 constant IC4y = 10915386471964999341016166937952548568058036159601535214565672698374193076432; + + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, r)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32))) + + g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64))) + + g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96))) + + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + checkField(calldataload(add(_pubSignals, 32))) + + checkField(calldataload(add(_pubSignals, 64))) + + checkField(calldataload(add(_pubSignals, 96))) + + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } + } From 72c2b08334ef697f274517daf2e000acdf0af5b3 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:49:39 +0530 Subject: [PATCH 03/19] feat: add gcp jwt verifier (#1674) --- .../verifiers/gcp/Verifier_gcp_jwt.sol | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol diff --git a/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol b/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol new file mode 100644 index 000000000..4d36ea61c --- /dev/null +++ b/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright 2021 0KIMS association. + + This file is generated with [snarkJS](https://github.com/iden3/snarkjs). + + snarkJS is a free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + snarkJS is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with snarkJS. If not, see . +*/ + +pragma solidity >=0.7.0 <0.9.0; + +contract Verifier_gcp_jwt { + // Scalar field size + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + // Base field size + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + // Verification Key data + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; + uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; + uint256 constant deltax1 = 1804222383802986733937376810902861143401033555807870231731929239915419049861; + uint256 constant deltax2 = 15902885537441599351050098769394227668772388058868388096316964244217496511682; + uint256 constant deltay1 = 4195707504005103778106485021796359604414786496137920116128130440872062477216; + uint256 constant deltay2 = 20513207510859042996645896574478474889840017920990203652675014165180462273668; + + + uint256 constant IC0x = 6972951741762339913362267428319005943611938060812676091174501911982947323821; + uint256 constant IC0y = 4968121098705797351946375443564156998686441710551907423285338106315203657372; + + uint256 constant IC1x = 3969479803545901558882616933276060341612655312403217371718193775571328202698; + uint256 constant IC1y = 10796516354190443333590906104065573186594421836191093099894208495600273943382; + + uint256 constant IC2x = 5282886783908067346990928387588210996099802199800176473402519317523182497411; + uint256 constant IC2y = 13420701105707643769706876856296866111708803407614711871170325095961081369695; + + uint256 constant IC3x = 14105950545034420261862110084277090993607573654064743638564927148396262651666; + uint256 constant IC3y = 13354956139782865997977495342720245140716772080136555810660173122394181127180; + + uint256 constant IC4x = 17223368406124250621460330134418760536341963146179581332507963390797809647912; + uint256 constant IC4y = 19015620010364835231555497011683709184643217460850880718542989960325995808017; + + uint256 constant IC5x = 11415362657438949221591074018468802007898322076011964898865456054649179831908; + uint256 constant IC5y = 17459573325598515038912928408360066384367356809087828399079121874232360528478; + + uint256 constant IC6x = 15574545936483334745596750909280550198515448424427848182054643607937078179213; + uint256 constant IC6y = 13006549512473282147197122913454973085866920937923147249375738521329287066222; + + uint256 constant IC7x = 14645989050046479540147134517500433000682841795623944679511623689017979403245; + uint256 constant IC7y = 16002146776744341769994596125501558460157837756621333957158039132600774201665; + + uint256 constant IC8x = 17447612904927318100653430764709204605475101707883725218472729377143326600248; + uint256 constant IC8y = 16892886274335002504909275077153679691684214526248560805118560019125943648821; + + uint256 constant IC9x = 17653661950237194880278154054792568909474176263902202958186273149474358670533; + uint256 constant IC9y = 11669219494719975955790450067861506164332870357879984076098486608481987018857; + + uint256 constant IC10x = 13289207501149959620194929372715676920560830325500657282490914929267428690980; + uint256 constant IC10y = 12465657438099014694334055521610703216229866770917539818266695642349007426072; + + uint256 constant IC11x = 18446654622136293276199162514838693836980616816456314636743905193625590745253; + uint256 constant IC11y = 12876916821064374752505779861869326377989533450827838519593872009453598320656; + + uint256 constant IC12x = 11001381773587677694421240176598022327285567125732057704900785068521955604564; + uint256 constant IC12y = 15721905323957520285870204323317542530315127175554829712351392669354944626115; + + uint256 constant IC13x = 19526090904722047042773905186611760547729403485756211734248157863388135796357; + uint256 constant IC13y = 6872421404352779784414693997079152972445035104903743503355279949152744176183; + + uint256 constant IC14x = 15194138441068760983236111544251338084740306295420897247383092303969333517280; + uint256 constant IC14y = 17571382599242644993857901274570230804168370452582601899367177574780143361956; + + uint256 constant IC15x = 584870595147362727880838486101127854955042037369856345600359023707849233383; + uint256 constant IC15y = 12343643073139461156226272211050331809098122200356986708169739203244290558425; + + uint256 constant IC16x = 14164891277783985284859197223195840777194061449283527719178608169082529731883; + uint256 constant IC16y = 5769361895392815047832493230313789373949187154386769492255962435984388734; + + uint256 constant IC17x = 5526583431755874525920531779957581117218605045719526246142282984128225259812; + uint256 constant IC17y = 15582261976988135470726322969910254124942972597198825965150134549937865280024; + + uint256 constant IC18x = 11933687532433713666089789805193821666211611847890385200532102102696090562695; + uint256 constant IC18y = 13768581020150988368938923899239734752213497676691170616636813895788587803927; + + uint256 constant IC19x = 21039243000302785560612608554208434709650210545299036143304628975668975303432; + uint256 constant IC19y = 3072044020424624557872621541718589400992098528118783904368755425332969903054; + + uint256 constant IC20x = 13029408846315391045768292892963336300734709802776968717851605403617397448869; + uint256 constant IC20y = 21441391199269244274037661931659719640029973634066921385003370500690694569608; + + + // Memory data + uint16 constant pVk = 0; + uint16 constant pPairing = 128; + + uint16 constant pLastMem = 896; + + function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[20] calldata _pubSignals) public view returns (bool) { + assembly { + function checkField(v) { + if iszero(lt(v, r)) { + mstore(0, 0) + return(0, 0x20) + } + } + + // G1 function to multiply a G1 value(x,y) to value in an address + function g1_mulAccC(pR, x, y, s) { + let success + let mIn := mload(0x40) + mstore(mIn, x) + mstore(add(mIn, 32), y) + mstore(add(mIn, 64), s) + + success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + + mstore(add(mIn, 64), mload(pR)) + mstore(add(mIn, 96), mload(add(pR, 32))) + + success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64) + + if iszero(success) { + mstore(0, 0) + return(0, 0x20) + } + } + + function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk { + let _pPairing := add(pMem, pPairing) + let _pVk := add(pMem, pVk) + + mstore(_pVk, IC0x) + mstore(add(_pVk, 32), IC0y) + + // Compute the linear combination vk_x + + g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0))) + + g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32))) + + g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64))) + + g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96))) + + g1_mulAccC(_pVk, IC5x, IC5y, calldataload(add(pubSignals, 128))) + + g1_mulAccC(_pVk, IC6x, IC6y, calldataload(add(pubSignals, 160))) + + g1_mulAccC(_pVk, IC7x, IC7y, calldataload(add(pubSignals, 192))) + + g1_mulAccC(_pVk, IC8x, IC8y, calldataload(add(pubSignals, 224))) + + g1_mulAccC(_pVk, IC9x, IC9y, calldataload(add(pubSignals, 256))) + + g1_mulAccC(_pVk, IC10x, IC10y, calldataload(add(pubSignals, 288))) + + g1_mulAccC(_pVk, IC11x, IC11y, calldataload(add(pubSignals, 320))) + + g1_mulAccC(_pVk, IC12x, IC12y, calldataload(add(pubSignals, 352))) + + g1_mulAccC(_pVk, IC13x, IC13y, calldataload(add(pubSignals, 384))) + + g1_mulAccC(_pVk, IC14x, IC14y, calldataload(add(pubSignals, 416))) + + g1_mulAccC(_pVk, IC15x, IC15y, calldataload(add(pubSignals, 448))) + + g1_mulAccC(_pVk, IC16x, IC16y, calldataload(add(pubSignals, 480))) + + g1_mulAccC(_pVk, IC17x, IC17y, calldataload(add(pubSignals, 512))) + + g1_mulAccC(_pVk, IC18x, IC18y, calldataload(add(pubSignals, 544))) + + g1_mulAccC(_pVk, IC19x, IC19y, calldataload(add(pubSignals, 576))) + + g1_mulAccC(_pVk, IC20x, IC20y, calldataload(add(pubSignals, 608))) + + + // -A + mstore(_pPairing, calldataload(pA)) + mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) + + // B + mstore(add(_pPairing, 64), calldataload(pB)) + mstore(add(_pPairing, 96), calldataload(add(pB, 32))) + mstore(add(_pPairing, 128), calldataload(add(pB, 64))) + mstore(add(_pPairing, 160), calldataload(add(pB, 96))) + + // alpha1 + mstore(add(_pPairing, 192), alphax) + mstore(add(_pPairing, 224), alphay) + + // beta2 + mstore(add(_pPairing, 256), betax1) + mstore(add(_pPairing, 288), betax2) + mstore(add(_pPairing, 320), betay1) + mstore(add(_pPairing, 352), betay2) + + // vk_x + mstore(add(_pPairing, 384), mload(add(pMem, pVk))) + mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) + + + // gamma2 + mstore(add(_pPairing, 448), gammax1) + mstore(add(_pPairing, 480), gammax2) + mstore(add(_pPairing, 512), gammay1) + mstore(add(_pPairing, 544), gammay2) + + // C + mstore(add(_pPairing, 576), calldataload(pC)) + mstore(add(_pPairing, 608), calldataload(add(pC, 32))) + + // delta2 + mstore(add(_pPairing, 640), deltax1) + mstore(add(_pPairing, 672), deltax2) + mstore(add(_pPairing, 704), deltay1) + mstore(add(_pPairing, 736), deltay2) + + + let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) + + isOk := and(success, mload(_pPairing)) + } + + let pMem := mload(0x40) + mstore(0x40, add(pMem, pLastMem)) + + // Validate that all evaluations ∈ F + + checkField(calldataload(add(_pubSignals, 0))) + + checkField(calldataload(add(_pubSignals, 32))) + + checkField(calldataload(add(_pubSignals, 64))) + + checkField(calldataload(add(_pubSignals, 96))) + + checkField(calldataload(add(_pubSignals, 128))) + + checkField(calldataload(add(_pubSignals, 160))) + + checkField(calldataload(add(_pubSignals, 192))) + + checkField(calldataload(add(_pubSignals, 224))) + + checkField(calldataload(add(_pubSignals, 256))) + + checkField(calldataload(add(_pubSignals, 288))) + + checkField(calldataload(add(_pubSignals, 320))) + + checkField(calldataload(add(_pubSignals, 352))) + + checkField(calldataload(add(_pubSignals, 384))) + + checkField(calldataload(add(_pubSignals, 416))) + + checkField(calldataload(add(_pubSignals, 448))) + + checkField(calldataload(add(_pubSignals, 480))) + + checkField(calldataload(add(_pubSignals, 512))) + + checkField(calldataload(add(_pubSignals, 544))) + + checkField(calldataload(add(_pubSignals, 576))) + + checkField(calldataload(add(_pubSignals, 608))) + + + // Validate all evaluations + let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) + + mstore(0, isValid) + return(0, 0x20) + } + } + } From f11e8606594c89a5b3e735a62c26c1600893fa25 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:45:56 +0530 Subject: [PATCH 04/19] fix: use pubsignals length of 20 (#1675) --- .../registry/IdentityRegistryKycImplV1.sol | 18 +++++++++--------- .../contracts/tests/MockGCPJWTVerifier.sol | 2 +- contracts/test/v2/registerKyc.test.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/contracts/contracts/registry/IdentityRegistryKycImplV1.sol b/contracts/contracts/registry/IdentityRegistryKycImplV1.sol index 437925cd1..feae4e144 100644 --- a/contracts/contracts/registry/IdentityRegistryKycImplV1.sol +++ b/contracts/contracts/registry/IdentityRegistryKycImplV1.sol @@ -92,7 +92,7 @@ interface IGCPJWTVerifier { uint256[2] calldata pA, uint256[2][2] calldata pB, uint256[2] calldata pC, - uint256[19] calldata pubSignals + uint256[20] calldata pubSignals ) external view returns (bool); } @@ -450,7 +450,7 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg uint256[2] calldata pA, uint256[2][2] calldata pB, uint256[2] calldata pC, - uint256[19] calldata pubSignals + uint256[20] calldata pubSignals ) external onlyProxy onlyTEE { // Check if the proof is valid if (!IGCPJWTVerifier(_gcpJwtVerifier).verifyProof(pA, pB, pC, pubSignals)) revert INVALID_PROOF(); @@ -459,19 +459,19 @@ contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityReg if (pubSignals[0] != _gcpRootCAPubkeyHash) revert INVALID_ROOT_CA(); // Check if the TEE image hash is valid - bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[4], pubSignals[5], pubSignals[6]); + bytes memory imageHash = GCPJWTHelper.unpackAndConvertImageHash(pubSignals[5], pubSignals[6], pubSignals[7]); if (!IPCR0Manager(_PCR0Manager).isPCR0Set(imageHash)) revert INVALID_IMAGE(); // Unpack the pubkey and register it uint256 pubkeyCommitment = GCPJWTHelper.unpackAndDecodeHexPubkey(pubSignals[1], pubSignals[2], pubSignals[3]); _isRegisteredPubkeyCommitment[pubkeyCommitment] = true; - uint256 currentYear = 2000 + pubSignals[7] * 10 + pubSignals[8]; - uint256 currentMonth = pubSignals[9] * 10 + pubSignals[10]; - uint256 currentDay = pubSignals[11] * 10 + pubSignals[12]; - uint256 currentHour = pubSignals[13] * 10 + pubSignals[14]; - uint256 currentMinute = pubSignals[15] * 10 + pubSignals[16]; - uint256 currentSecond = pubSignals[17] * 10 + pubSignals[18]; + uint256 currentYear = 2000 + pubSignals[8] * 10 + pubSignals[9]; + uint256 currentMonth = pubSignals[10] * 10 + pubSignals[11]; + uint256 currentDay = pubSignals[12] * 10 + pubSignals[13]; + uint256 currentHour = pubSignals[14] * 10 + pubSignals[15]; + uint256 currentMinute = pubSignals[16] * 10 + pubSignals[17]; + uint256 currentSecond = pubSignals[18] * 10 + pubSignals[19]; uint256 currentTimestamp = Formatter.toTimeStampWithSeconds( currentYear, currentMonth, diff --git a/contracts/contracts/tests/MockGCPJWTVerifier.sol b/contracts/contracts/tests/MockGCPJWTVerifier.sol index 318323352..81708a185 100644 --- a/contracts/contracts/tests/MockGCPJWTVerifier.sol +++ b/contracts/contracts/tests/MockGCPJWTVerifier.sol @@ -29,7 +29,7 @@ contract MockGCPJWTVerifier { uint256[2] calldata pA, uint256[2][2] calldata pB, uint256[2] calldata pC, - uint256[19] calldata pubSignals + uint256[20] calldata pubSignals ) external view returns (bool) { // Silence unused variable warnings pA; diff --git a/contracts/test/v2/registerKyc.test.ts b/contracts/test/v2/registerKyc.test.ts index 96e3775bc..42db8ba30 100644 --- a/contracts/test/v2/registerKyc.test.ts +++ b/contracts/test/v2/registerKyc.test.ts @@ -128,6 +128,7 @@ describe("KYC Registration test", function () { p0, p1, p2, + 0n, testImageHash.p0, testImageHash.p1, testImageHash.p2, @@ -242,6 +243,7 @@ describe("KYC Registration test", function () { 1n, 2n, 3n, + 0n, 4n, 5n, 6n, @@ -273,6 +275,7 @@ describe("KYC Registration test", function () { 1n, 2n, 3n, + 0n, 4n, 5n, 6n, @@ -322,6 +325,7 @@ describe("KYC Registration test", function () { 1n, 2n, 3n, + 0n, 4n, 5n, 6n, @@ -356,6 +360,7 @@ describe("KYC Registration test", function () { p0, p1, p2, + 0n, 177384435506496807268973340845468654286294928521500580044819492874465981028n, 175298970718174405520284770870231222447414486446296682893283627688949855078n, 13360n, @@ -379,6 +384,7 @@ describe("KYC Registration test", function () { p0, p1, p2, + 0n, 177384435506496807268973340845468654286294928521500580044819492874465981028n, 175298970718174405520284770870231222447414486446296682893283627688949855078n, 13360n, @@ -417,6 +423,7 @@ describe("KYC Registration test", function () { 1n, 2n, 3n, + 0n, 4n, 5n, 6n, @@ -434,6 +441,7 @@ describe("KYC Registration test", function () { 1n, 2n, 3n, + 0n, 4n, 5n, 6n, @@ -451,6 +459,7 @@ describe("KYC Registration test", function () { 1n, 2n, 3n, + 0n, 4n, 5n, 6n, From a6c84d80f7a3e58905ec367cb9e25b576b8a0655 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Fri, 30 Jan 2026 18:35:32 +0100 Subject: [PATCH 05/19] feat(kyc): register fcm token for sumsub verification (#1673) * feat(kyc): register fcm token for sumsub verification * fix tests * remove unused import * fix lint --- app/env.ts | 2 +- app/src/navigation/index.tsx | 7 +- app/src/providers/selfClientProvider.tsx | 4 +- app/src/screens/kyc/KycSuccessScreen.tsx | 47 ++++- .../notifications/notificationService.ts | 5 + .../src/screens/kyc/KycSuccessScreen.test.tsx | 196 +++++++++++++++++- 6 files changed, 240 insertions(+), 21 deletions(-) diff --git a/app/env.ts b/app/env.ts index c5e041280..52eacf506 100644 --- a/app/env.ts +++ b/app/env.ts @@ -27,6 +27,7 @@ export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true'; export const MIXPANEL_NFC_PROJECT_TOKEN = undefined; export const SEGMENT_KEY = process.env.SEGMENT_KEY; +export const SELF_UUID_NAMESPACE = process.env.SELF_UUID_NAMESPACE; export const SENTRY_DSN = process.env.SENTRY_DSN; export const SUMSUB_TEE_URL = process.env.SUMSUB_TEE_URL || 'http://localhost:8080'; @@ -34,6 +35,5 @@ export const SUMSUB_TEST_TOKEN = process.env.SUMSUB_TEST_TOKEN; export const TURNKEY_AUTH_PROXY_CONFIG_ID = process.env.TURNKEY_AUTH_PROXY_CONFIG_ID; - export const TURNKEY_GOOGLE_CLIENT_ID = process.env.TURNKEY_GOOGLE_CLIENT_ID; export const TURNKEY_ORGANIZATION_ID = process.env.TURNKEY_ORGANIZATION_ID; diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index 17f90209b..d7dfb4ed5 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -79,6 +79,7 @@ export type RootStackParamList = Omit< | 'Home' | 'IDPicker' | 'IdDetails' + | 'KycSuccess' | 'RegistrationFallback' | 'Loading' | 'Modal' @@ -201,7 +202,11 @@ export type RootStackParamList = Omit< // Onboarding screens Disclaimer: undefined; - KycSuccess: undefined; + KycSuccess: + | { + userId?: string; + } + | undefined; // Dev screens CreateMock: undefined; diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index 28f9abe5f..6bfbc9c2e 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -380,7 +380,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { // Success case: navigate to KYC success screen if (navigationRef.isReady()) { - navigationRef.navigate('KycSuccess'); + navigationRef.navigate('KycSuccess', { + userId: accessToken.userId, + }); } } catch (error) { const safeInitError = sanitizeErrorMessage( diff --git a/app/src/screens/kyc/KycSuccessScreen.tsx b/app/src/screens/kyc/KycSuccessScreen.tsx index 22849692c..3269da141 100644 --- a/app/src/screens/kyc/KycSuccessScreen.tsx +++ b/app/src/screens/kyc/KycSuccessScreen.tsx @@ -2,37 +2,70 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React from 'react'; +import React, { useCallback } from 'react'; import { StyleSheet, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { YStack } from 'tamagui'; +import { v5 as uuidv5 } from 'uuid'; +import type { StaticScreenProps } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha'; +import { DelayedLottieView, useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json'; import { AbstractButton, Description, Title, } from '@selfxyz/mobile-sdk-alpha/components'; +import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors'; import { buttonTap } from '@/integrations/haptics'; import type { RootStackParamList } from '@/navigation'; -import { requestNotificationPermission } from '@/services/notifications/notificationService'; +import { + getFCMToken, + getSelfUuidNamespace, + registerDeviceToken, + requestNotificationPermission, +} from '@/services/notifications/notificationService'; +import { useSettingStore } from '@/stores/settingStore'; -const KycSuccessScreen: React.FC = () => { +type KycSuccessRouteParams = StaticScreenProps< + | { + userId?: string; + } + | undefined +>; + +const KycSuccessScreen: React.FC = ({ + route: { params }, +}) => { const navigation = useNavigation>(); + const userId = params?.userId; const insets = useSafeAreaInsets(); + const setFcmToken = useSettingStore(state => state.setFcmToken); + const selfClient = useSelfClient(); + const { trackEvent } = selfClient; - const handleReceiveUpdates = async () => { + const handleReceiveUpdates = useCallback(async () => { buttonTap(); - await requestNotificationPermission(); + + if ((await requestNotificationPermission()) && userId) { + const token = await getFCMToken(); + if (token) { + setFcmToken(token); + trackEvent(ProofEvents.FCM_TOKEN_STORED); + + const sessionId = uuidv5(userId, getSelfUuidNamespace()); + await registerDeviceToken(sessionId, token); + } + } + // Navigate to Home regardless of permission result navigation.navigate('Home', {}); - }; + }, [navigation, setFcmToken, trackEvent, userId]); const handleCheckLater = () => { buttonTap(); diff --git a/app/src/services/notifications/notificationService.ts b/app/src/services/notifications/notificationService.ts index 87fe09ec6..159201e34 100644 --- a/app/src/services/notifications/notificationService.ts +++ b/app/src/services/notifications/notificationService.ts @@ -3,6 +3,7 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import { PermissionsAndroid, Platform } from 'react-native'; +import { SELF_UUID_NAMESPACE } from '@env'; import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import messaging from '@react-native-firebase/messaging'; @@ -36,6 +37,10 @@ const error = (...args: unknown[]) => { if (!isTestEnv) console.error(...args); }; +export function getSelfUuidNamespace(): string { + return SELF_UUID_NAMESPACE ?? ''; +} + export { getStateMessage }; export async function isNotificationSystemReady(): Promise<{ diff --git a/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx b/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx index ea8cc6c72..48d37a7d7 100644 --- a/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx +++ b/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx @@ -3,8 +3,9 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import React from 'react'; +import { v5 as uuidv5 } from 'uuid'; import { useNavigation } from '@react-navigation/native'; -import { render } from '@testing-library/react-native'; +import { fireEvent, render, waitFor } from '@testing-library/react-native'; import ErrorBoundary from '@/components/ErrorBoundary'; import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen'; @@ -46,10 +47,6 @@ jest.mock('tamagui', () => ({ Text: ({ children, ...props }: any) => {children}, })); -jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ - DelayedLottieView: () => null, -})); - jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({ black: '#000000', white: '#FFFFFF', @@ -57,17 +54,17 @@ jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({ jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({ AbstractButton: ({ children, onPress }: any) => ( - ), PrimaryButton: ({ children, onPress }: any) => ( - ), SecondaryButton: ({ children, onPress }: any) => ( - ), @@ -77,12 +74,23 @@ jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({ ), })); +jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({ + ProofEvents: { + FCM_TOKEN_STORED: 'FCM_TOKEN_STORED', + }, +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json', () => ({})); + jest.mock('@/integrations/haptics', () => ({ buttonTap: jest.fn(), })); jest.mock('@/services/notifications/notificationService', () => ({ requestNotificationPermission: jest.fn(), + getFCMToken: jest.fn(), + registerDeviceToken: jest.fn(), + getSelfUuidNamespace: jest.fn(() => '1eebc0f5-eee9-45a4-9474-a0d103b9f20c'), })); jest.mock('@/config/sentry', () => ({ @@ -94,12 +102,36 @@ jest.mock('@/services/analytics', () => ({ trackNfcEvent: jest.fn(), })); +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + DelayedLottieView: () => null, + useSelfClient: jest.fn(), +})); + +jest.mock('@/stores/settingStore', () => ({ + useSettingStore: jest.fn(), +})); + const mockUseNavigation = useNavigation as jest.MockedFunction< typeof useNavigation >; +// Import mocked modules +const { useSelfClient } = jest.requireMock('@selfxyz/mobile-sdk-alpha'); +const { useSettingStore } = jest.requireMock('@/stores/settingStore'); + +const MOCK_SELF_UUID_NAMESPACE = '1eebc0f5-eee9-45a4-9474-a0d103b9f20c'; + describe('KycSuccessScreen', () => { const mockNavigate = jest.fn(); + const mockTrackEvent = jest.fn(); + const mockSetFcmToken = jest.fn(); + const mockUserId = '19f21362-856a-4606-88e1-fa306036978f'; + const mockFcmToken = 'mock-fcm-token'; + const mockRoute = { + params: { + userId: mockUserId, + }, + }; beforeEach(() => { jest.clearAllMocks(); @@ -107,23 +139,165 @@ describe('KycSuccessScreen', () => { mockUseNavigation.mockReturnValue({ navigate: mockNavigate, } as any); + + useSelfClient.mockReturnValue({ + trackEvent: mockTrackEvent, + }); + + useSettingStore.mockReturnValue(mockSetFcmToken); + + ( + notificationService.requestNotificationPermission as jest.Mock + ).mockResolvedValue(true); + (notificationService.getFCMToken as jest.Mock).mockResolvedValue( + mockFcmToken, + ); + (notificationService.registerDeviceToken as jest.Mock).mockResolvedValue( + undefined, + ); }); it('should render the screen without errors', () => { - const { root } = render(); + const { root } = render(); expect(root).toBeTruthy(); }); it('should have navigation available', () => { - render(); + render(); expect(mockUseNavigation).toHaveBeenCalled(); }); it('should have notification service available', () => { - render(); + render(); expect(notificationService.requestNotificationPermission).toBeDefined(); }); + it('should fetch and register FCM token when "Receive live updates" is pressed', async () => { + const { root } = render(); + + const buttons = root.findAllByType('button'); + const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates" + fireEvent.press(receiveUpdatesButton); + + await waitFor(() => { + // Verify notification permission was requested + expect( + notificationService.requestNotificationPermission, + ).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + // Verify FCM token was fetched + expect(notificationService.getFCMToken).toHaveBeenCalledTimes(1); + }); + + await waitFor(() => { + // Verify FCM token was stored in settings store + expect(mockSetFcmToken).toHaveBeenCalledWith(mockFcmToken); + }); + + await waitFor(() => { + // Verify tracking event was sent + expect(mockTrackEvent).toHaveBeenCalledWith('FCM_TOKEN_STORED'); + }); + + await waitFor(() => { + // Verify device token was registered with deterministic session ID + expect(notificationService.registerDeviceToken).toHaveBeenCalledWith( + uuidv5(mockUserId, MOCK_SELF_UUID_NAMESPACE), + mockFcmToken, + ); + }); + + await waitFor(() => { + // Verify navigation to Home screen + expect(mockNavigate).toHaveBeenCalledWith('Home', {}); + }); + }); + + it('should navigate to Home without FCM token when permission is denied', async () => { + ( + notificationService.requestNotificationPermission as jest.Mock + ).mockResolvedValue(false); + + const { root } = render(); + + const buttons = root.findAllByType('button'); + const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates" + fireEvent.press(receiveUpdatesButton); + + await waitFor(() => { + // Verify notification permission was requested + expect( + notificationService.requestNotificationPermission, + ).toHaveBeenCalledTimes(1); + }); + + // Verify FCM token was NOT fetched + expect(notificationService.getFCMToken).not.toHaveBeenCalled(); + + // Verify FCM token was NOT stored + expect(mockSetFcmToken).not.toHaveBeenCalled(); + + // Verify device token was NOT registered + expect(notificationService.registerDeviceToken).not.toHaveBeenCalled(); + + await waitFor(() => { + // Verify navigation to Home screen still happens + expect(mockNavigate).toHaveBeenCalledWith('Home', {}); + }); + }); + + it('should navigate to Home when "I will check back later" is pressed', () => { + const { root } = render(); + + const buttons = root.findAllByType('button'); + const checkLaterButton = buttons[1]; // Second button is "I will check back later" + fireEvent.press(checkLaterButton); + + // Verify navigation to Home screen + expect(mockNavigate).toHaveBeenCalledWith('Home', {}); + + // Verify FCM-related functions were NOT called + expect( + notificationService.requestNotificationPermission, + ).not.toHaveBeenCalled(); + expect(notificationService.getFCMToken).not.toHaveBeenCalled(); + expect(mockSetFcmToken).not.toHaveBeenCalled(); + expect(notificationService.registerDeviceToken).not.toHaveBeenCalled(); + }); + + it('should handle missing userId gracefully', async () => { + const routeWithoutUserId = { + params: {}, + }; + + ( + notificationService.requestNotificationPermission as jest.Mock + ).mockResolvedValue(true); + + const { root } = render(); + + const buttons = root.findAllByType('button'); + const receiveUpdatesButton = buttons[0]; // First button is "Receive live updates" + fireEvent.press(receiveUpdatesButton); + + await waitFor(() => { + // Verify notification permission was requested + expect( + notificationService.requestNotificationPermission, + ).toHaveBeenCalledTimes(1); + }); + + // Verify FCM token was NOT fetched (no userId) + expect(notificationService.getFCMToken).not.toHaveBeenCalled(); + + await waitFor(() => { + // Verify navigation to Home screen still happens + expect(mockNavigate).toHaveBeenCalledWith('Home', {}); + }); + }); + it('renders fallback on render error', () => { // Mock console.error to suppress error boundary error logs during test const consoleErrorSpy = jest From a96777d80a43b1dbfc8e415f91def378a537591f Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 30 Jan 2026 10:25:51 -0800 Subject: [PATCH 06/19] ux: add country document json info as static asset (#1670) * add country document json info as static asset * add staleness test * update test * formatting --- .../src/data/country-document-types.json | 256 ++++++++++++++++++ .../src/documents/useCountries.tsx | 37 +-- .../country-data-sync.integration.test.ts | 99 +++++++ 3 files changed, 361 insertions(+), 31 deletions(-) create mode 100644 packages/mobile-sdk-alpha/src/data/country-document-types.json create mode 100644 packages/mobile-sdk-alpha/tests/data/country-data-sync.integration.test.ts diff --git a/packages/mobile-sdk-alpha/src/data/country-document-types.json b/packages/mobile-sdk-alpha/src/data/country-document-types.json new file mode 100644 index 000000000..a535f1829 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/data/country-document-types.json @@ -0,0 +1,256 @@ +{ + "ABW": ["p", "i"], + "AFG": ["p"], + "AGO": ["p", "i"], + "AIA": ["p", "i"], + "ALA": ["p", "i"], + "ALB": ["p", "i"], + "AND": ["p", "i"], + "ARE": ["p", "i"], + "ARG": ["p", "i"], + "ARM": ["p", "i"], + "ASM": ["p", "i"], + "ATA": ["p", "i"], + "ATF": ["p", "i"], + "ATG": ["p", "i"], + "AUS": ["p", "i"], + "AUT": ["p", "i"], + "AZE": ["p", "i"], + "BDI": ["p", "i"], + "BEL": ["p", "i"], + "BEN": ["p", "i"], + "BES": ["p", "i"], + "BFA": ["p", "i"], + "BGD": ["p", "i"], + "BGR": ["p", "i"], + "BHR": ["p", "i"], + "BHS": ["p", "i"], + "BIH": ["p", "i"], + "BLM": ["p", "i"], + "BLR": ["p", "i"], + "BLZ": ["p", "i"], + "BMU": ["p", "i"], + "BOL": ["p", "i"], + "BRA": ["p", "i"], + "BRB": ["p", "i"], + "BRN": ["p", "i"], + "BTN": ["p", "i"], + "BVT": ["p", "i"], + "BWA": ["p", "i"], + "CAF": ["p", "i"], + "CAN": ["p", "i"], + "CCK": ["p", "i"], + "CHE": ["p", "i"], + "CHL": ["p", "i"], + "CHN": ["p", "i"], + "CIV": ["p", "i"], + "CMR": ["p", "i"], + "COD": ["p", "i"], + "COG": ["p", "i"], + "COK": ["p", "i"], + "COL": ["p", "i"], + "COM": ["p", "i"], + "CPV": ["p", "i"], + "CRI": ["p", "i"], + "CUB": ["p", "i"], + "CUW": ["p", "i"], + "CXR": ["p", "i"], + "CYM": ["p", "i"], + "CYP": ["p", "i"], + "CZE": ["p", "i"], + "D<<": ["p", "i"], + "DJI": ["p", "i"], + "DMA": ["p", "i"], + "DNK": ["p", "i"], + "DOM": ["p", "i"], + "DZA": ["p", "i"], + "ECU": ["p", "i"], + "EGY": [], + "ERI": ["p", "i"], + "ESH": ["p", "i"], + "ESP": ["p", "i"], + "EST": ["p", "i"], + "ETH": ["p", "i"], + "EUE": ["p", "i"], + "FIN": ["p", "i"], + "FJI": ["p", "i"], + "FLK": ["p", "i"], + "FRA": ["p", "i"], + "FRO": ["p", "i"], + "FSM": ["p", "i"], + "GAB": ["p", "i"], + "GBR": ["p", "i"], + "GEO": ["p", "i"], + "GGY": ["p", "i"], + "GHA": ["p", "i"], + "GIB": ["p", "i"], + "GIN": ["p", "i"], + "GLP": ["p", "i"], + "GMB": ["p", "i"], + "GNB": ["p", "i"], + "GNQ": ["p", "i"], + "GRC": ["p", "i"], + "GRD": ["p", "i"], + "GRL": ["p", "i"], + "GTM": ["p", "i"], + "GUF": ["p", "i"], + "GUM": ["p", "i"], + "GUY": ["p", "i"], + "HKG": ["p", "i"], + "HMD": ["p", "i"], + "HND": ["p", "i"], + "HRV": ["p", "i"], + "HTI": ["p", "i"], + "HUN": ["p", "i"], + "IDN": ["p", "i"], + "IMN": ["p", "i"], + "IND": ["p", "a"], + "IOT": ["p", "i"], + "IRL": ["p", "i"], + "IRN": ["p", "i"], + "IRQ": ["p", "i"], + "ISL": ["p", "i"], + "ISR": ["p", "i"], + "ITA": ["p", "i"], + "JAM": ["p", "i"], + "JEY": ["p", "i"], + "JOR": ["p", "i"], + "JPN": ["p", "i"], + "KAZ": ["p", "i"], + "KEN": ["p", "i"], + "KGZ": ["p", "i"], + "KHM": ["p", "i"], + "KIR": ["p", "i"], + "KNA": ["p", "i"], + "KOR": ["p", "i"], + "KWT": ["p", "i"], + "LAO": ["p", "i"], + "LBN": ["p", "i"], + "LBR": ["p", "i"], + "LBY": ["p", "i"], + "LCA": ["p", "i"], + "LIE": ["p", "i"], + "LKA": ["p", "i"], + "LSO": ["p", "i"], + "LTU": ["p", "i"], + "LUX": ["p", "i"], + "LVA": ["p", "i"], + "MAC": ["p", "i"], + "MAF": ["p", "i"], + "MAR": ["p", "i"], + "MCO": ["p", "i"], + "MDA": ["p", "i"], + "MDG": ["p", "i"], + "MDV": ["p", "i"], + "MEX": ["p", "i"], + "MHL": ["p", "i"], + "MKD": ["p", "i"], + "MLI": ["p", "i"], + "MLT": ["p", "i"], + "MMR": ["p", "i"], + "MNE": ["p", "i"], + "MNG": ["p", "i"], + "MNP": ["p", "i"], + "MOZ": ["p", "i"], + "MRT": ["p", "i"], + "MSR": ["p", "i"], + "MTQ": ["p", "i"], + "MUS": ["p", "i"], + "MWI": ["p", "i"], + "MYS": ["p", "i"], + "MYT": ["p", "i"], + "NAM": ["p", "i"], + "NCL": ["p", "i"], + "NER": ["p", "i"], + "NFK": ["p", "i"], + "NGA": ["p", "i"], + "NIC": ["p", "i"], + "NIU": ["p", "i"], + "NLD": ["p", "i"], + "NOR": ["p", "i"], + "NPL": ["p", "i"], + "NRU": ["p", "i"], + "NZL": ["p", "i"], + "OMN": ["p", "i"], + "PAK": ["p", "i"], + "PAN": ["p", "i"], + "PCN": ["p", "i"], + "PER": ["p", "i"], + "PHL": ["p", "i"], + "PLW": ["p", "i"], + "PNG": ["p", "i"], + "POL": ["p", "i"], + "PRI": ["p", "i"], + "PRK": ["p", "i"], + "PRT": ["p", "i"], + "PRY": ["p", "i"], + "PSE": ["p", "i"], + "PYF": ["p", "i"], + "QAT": ["p", "i"], + "REU": ["p", "i"], + "ROU": ["p", "i"], + "RUS": ["p", "i"], + "RWA": ["p", "i"], + "SAU": ["p", "i"], + "SDN": ["p", "i"], + "SEN": ["p", "i"], + "SGP": ["p", "i"], + "SGS": ["p", "i"], + "SHN": ["p", "i"], + "SJM": ["p", "i"], + "SLB": ["p", "i"], + "SLE": ["p", "i"], + "SLV": ["p", "i"], + "SMR": ["p", "i"], + "SOM": ["p", "i"], + "SPM": ["p", "i"], + "SRB": ["p", "i"], + "SSD": ["p", "i"], + "STP": ["p", "i"], + "SUR": ["p", "i"], + "SVK": ["p", "i"], + "SVN": ["p", "i"], + "SWE": ["p", "i"], + "SWZ": ["p", "i"], + "SXM": ["p", "i"], + "SYC": ["p", "i"], + "SYR": ["p", "i"], + "TCA": ["p", "i"], + "TCD": ["p", "i"], + "TGO": ["p", "i"], + "THA": ["p", "i"], + "TJK": ["p", "i"], + "TKL": ["p", "i"], + "TKM": ["p", "i"], + "TLS": ["p", "i"], + "TON": ["p", "i"], + "TTO": ["p", "i"], + "TUN": ["p", "i"], + "TUR": ["p", "i"], + "TUV": ["p", "i"], + "TWN": ["p", "i"], + "TZA": ["p", "i"], + "UGA": ["p", "i"], + "UKR": ["p", "i"], + "UMI": ["p", "i"], + "UNO": ["p", "i"], + "URY": ["p", "i"], + "USA": ["p", "i"], + "UZB": ["p", "i"], + "VAT": ["p", "i"], + "VCT": ["p", "i"], + "VEN": ["p", "i"], + "VGB": ["p", "i"], + "VIR": ["p", "i"], + "VNM": ["p", "i"], + "VUT": ["p", "i"], + "WLF": ["p", "i"], + "WSM": ["p", "i"], + "XCE": ["p", "i"], + "XOM": ["p", "i"], + "XPO": ["p", "i"], + "YEM": ["p", "i"], + "ZAF": ["p", "i"], + "ZMB": ["p", "i"], + "ZWE": ["p", "i"] +} diff --git a/packages/mobile-sdk-alpha/src/documents/useCountries.tsx b/packages/mobile-sdk-alpha/src/documents/useCountries.tsx index f8adae9e2..67acccc00 100644 --- a/packages/mobile-sdk-alpha/src/documents/useCountries.tsx +++ b/packages/mobile-sdk-alpha/src/documents/useCountries.tsx @@ -2,12 +2,14 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import { useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { getCountry } from 'react-native-localize'; import { commonNames } from '@selfxyz/common'; import { alpha2ToAlpha3 } from '@selfxyz/common/constants/countries'; +import countryDocumentTypesData from '../data/country-document-types.json'; + export interface CountryData { [countryCode: string]: string[]; } @@ -29,38 +31,11 @@ function getUserCountryCode(): string | null { } return null; } + export function useCountries() { - const [countryData, setCountryData] = useState({}); - const [loading, setLoading] = useState(true); + const countryData = countryDocumentTypesData as CountryData; const userCountryCode = useMemo(getUserCountryCode, []); - useEffect(() => { - const controller = new AbortController(); - const fetchCountryData = async () => { - try { - const response = await fetch('https://api.staging.self.xyz/id-picker', { - signal: controller.signal, - }); - const result = await response.json(); - - if (result.status === 'success') { - setCountryData(result.data); - // if (__DEV__) { - // console.log('Set country data:', result.data); - // } - } else { - console.error('API returned non-success status:', result.status); - } - } catch (error) { - console.error('Error fetching country data:', error); - } finally { - setLoading(false); - } - }; - fetchCountryData(); - return () => controller.abort(); - }, []); - const countryList = useMemo(() => { const allCountries = Object.keys(countryData).map(countryCode => ({ key: countryCode, @@ -77,5 +52,5 @@ export function useCountries() { const showSuggestion = userCountryCode && countryData[userCountryCode]; - return { countryData, countryList, loading, userCountryCode, showSuggestion }; + return { countryData, countryList, loading: false, userCountryCode, showSuggestion }; } diff --git a/packages/mobile-sdk-alpha/tests/data/country-data-sync.integration.test.ts b/packages/mobile-sdk-alpha/tests/data/country-data-sync.integration.test.ts new file mode 100644 index 000000000..76be2be58 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/data/country-data-sync.integration.test.ts @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +/** + * Integration test for country data synchronization. + * + * This test verifies that the bundled country-document-types.json matches + * the staging API response. It gracefully skips when network is unavailable + * to avoid CI flakiness from transient network issues. + * + * To run integration tests only: yarn test --grep="integration" + * To skip integration tests: yarn test --grep="^(?!.*integration)" + */ + +import { describe, expect, it } from 'vitest'; + +import countryDocumentTypesData from '../../src/data/country-document-types.json'; + +/** + * Helper to check if an error is a network-related error that should cause + * the test to skip rather than fail. + */ +function isNetworkError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + + const networkErrorPatterns = [ + 'ENOTFOUND', // DNS resolution failed + 'ECONNREFUSED', // Connection refused + 'ECONNRESET', // Connection reset + 'ETIMEDOUT', // Connection timed out + 'EAI_AGAIN', // DNS temporary failure + 'ENETUNREACH', // Network unreachable + 'EHOSTUNREACH', // Host unreachable + 'fetch failed', // Generic fetch failure + 'network', // Generic network error + 'AbortError', // Request aborted (timeout) + ]; + + const errorMessage = error.message.toLowerCase(); + const errorName = error.name; + + return networkErrorPatterns.some( + pattern => + errorMessage.includes(pattern.toLowerCase()) || + errorName === pattern || + ('cause' in error && + error.cause instanceof Error && + error.cause.message.toLowerCase().includes(pattern.toLowerCase())), + ); +} + +describe('Country data synchronization [integration]', () => { + it('bundled data should match API response', async ({ skip }) => { + // Fetch current data from staging API with timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + let response: Response; + try { + response = await fetch('https://api.staging.self.xyz/id-picker', { + signal: controller.signal, + }); + } catch (error) { + // Network errors should skip the test, not fail it + if (isNetworkError(error)) { + skip(); + return; + } + throw error; + } finally { + clearTimeout(timeoutId); + } + + // Non-2xx responses that aren't network errors should also skip + // (e.g., 503 Service Unavailable, 502 Bad Gateway) + if (!response.ok) { + if (response.status >= 500) { + skip(); + return; + } + // 4xx errors are likely real issues, so we let them fail + expect.fail(`API returned ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + expect(result.status).toBe('success'); + + const apiData = result.data; + const bundledData = countryDocumentTypesData; + + // Compare the data structures + expect(bundledData).toEqual(apiData); + + // If this test fails, it means the API has been updated with new countries + // or document types that aren't in the bundled data yet. + // To fix: Update src/data/country-document-types.json with the latest API data. + }, 10000); // 10s Vitest timeout +}); From 368799378078b4b18a58e1574b29c88a1c159eef Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Fri, 30 Jan 2026 13:06:08 -0800 Subject: [PATCH 07/19] fix failing test (#1677) --- .github/workflows/mobile-sdk-demo-e2e.yml | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/mobile-sdk-demo-e2e.yml b/.github/workflows/mobile-sdk-demo-e2e.yml index bc179734a..c90b94af1 100644 --- a/.github/workflows/mobile-sdk-demo-e2e.yml +++ b/.github/workflows/mobile-sdk-demo-e2e.yml @@ -459,6 +459,37 @@ jobs: FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \ xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath packages/mobile-sdk-demo/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule || { echo "❌ iOS build failed"; exit 1; } echo "✅ iOS build succeeded" + - name: Build iOS Release Archive (unsigned) + run: | + echo "Building iOS Release archive (unsigned) to validate Release configuration..." + WORKSPACE_PATH="${{ env.IOS_WORKSPACE_PATH }}" + + FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \ + xcodebuild archive \ + -workspace "$WORKSPACE_PATH" \ + -scheme ${{ env.IOS_PROJECT_SCHEME }} \ + -configuration Release \ + -archivePath packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive \ + -destination "generic/platform=iOS" \ + -jobs "$(sysctl -n hw.ncpu)" \ + -parallelizeTargets \ + -quiet \ + COMPILER_INDEX_STORE_ENABLE=NO \ + SWIFT_COMPILATION_MODE=wholemodule \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + AD_HOC_CODE_SIGNING_ALLOWED=NO \ + || { echo "❌ iOS Release archive build failed"; exit 1; } + echo "✅ iOS Release archive build succeeded (unsigned)" + + # Verify archive was created + if [ -d "packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive" ]; then + echo "📦 Archive created at packages/mobile-sdk-demo/ios/build/SelfDemoApp.xcarchive" + else + echo "❌ Archive not found" + exit 1 + fi - name: Install and Test on iOS run: | echo "Installing app on simulator..." From 6ef0dfdbb2c0760ac8f59b70fc77b98b63855636 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:52:32 +0530 Subject: [PATCH 08/19] deploy: kyc scripts and addresses for staging (#1679) --- contracts/hardhat.config.ts | 8 +- .../chain-11142220/deployed_addresses.json | 9 +- .../ignition/modules/hub/updateRegistries.ts | 6 +- .../ignition/modules/hub/updateVerifiers.ts | 5 + .../modules/registry/deployKycRegistry.ts | 52 +++++++ .../modules/registry/updateRegistries.ts | 146 +++++++++++------- .../modules/verifiers/deployAllVerifiers.ts | 6 +- 7 files changed, 172 insertions(+), 60 deletions(-) create mode 100644 contracts/ignition/modules/registry/deployKycRegistry.ts diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 89f1ef2d3..a5ffaf728 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -76,10 +76,10 @@ const config: HardhatUserConfig = { }, }, etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY as string, - // apiKey: { - // "celo-sepolia": process.env.ETHERSCAN_API_KEY as string, - // }, + // apiKey: process.env.ETHERSCAN_API_KEY as string, + apiKey: { + "celo-sepolia": process.env.ETHERSCAN_API_KEY as string, + }, customChains: [ { network: "celo", diff --git a/contracts/ignition/deployments/chain-11142220/deployed_addresses.json b/contracts/ignition/deployments/chain-11142220/deployed_addresses.json index 85593d9e3..a80ca80fe 100644 --- a/contracts/ignition/deployments/chain-11142220/deployed_addresses.json +++ b/contracts/ignition/deployments/chain-11142220/deployed_addresses.json @@ -97,5 +97,12 @@ "DeployHubV2#IdentityVerificationHub": "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74", "DeployNewHubAndUpgradee#IdentityVerificationHubV2": "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74", "DeployNewHubAndUpgradee#CustomVerifier": "0x2711E535D68D8B8729a7d126fEb13aEc0fe29A27", - "DeployNewHubAndUpgradee#IdentityVerificationHubImplV2": "0x48985ec4f71cBC8f387c5C77143110018560c7eD" + "DeployNewHubAndUpgradee#IdentityVerificationHubImplV2": "0x48985ec4f71cBC8f387c5C77143110018560c7eD", + "DeployKycRegistryModule#PCR0Manager": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2", + "DeployKycRegistryModule#PoseidonT3": "0x163983BAe19dE94A007C6C502b7389F6C359C818", + "DeployKycRegistryModule#Verifier_gcp_jwt": "0x13ee8CEa15a262D81a245b37889F7b4bEd015f4c", + "DeployKycRegistryModule#IdentityRegistryKycImplV1": "0x94f6DE38E10140B9E3963a770B5B769b38459a3B", + "DeployKycRegistryModule#IdentityRegistry": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3", + "UpdateAllRegistries#PCR0Manager": "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2", + "UpdateAllRegistries#a3": "0x90e907E4AaB6e9bcFB94997Af4A097e8CAadBdf3" } diff --git a/contracts/ignition/modules/hub/updateRegistries.ts b/contracts/ignition/modules/hub/updateRegistries.ts index 6468c0a04..29754b337 100644 --- a/contracts/ignition/modules/hub/updateRegistries.ts +++ b/contracts/ignition/modules/hub/updateRegistries.ts @@ -8,13 +8,15 @@ const AttestationId = { E_PASSPORT: "0x0000000000000000000000000000000000000000000000000000000000000001", EU_ID_CARD: "0x0000000000000000000000000000000000000000000000000000000000000002", AADHAAR: "0x0000000000000000000000000000000000000000000000000000000000000003", + KYC: "0x0000000000000000000000000000000000000000000000000000000000000004", }; // Map registry deployment modules to their attestation IDs const registryToAttestationId: Record = { // "DeployRegistryModule#IdentityRegistry": AttestationId.E_PASSPORT, // "DeployIdCardRegistryModule#IdentityRegistry": AttestationId.EU_ID_CARD, - "DeployAadhaarRegistryModule#IdentityRegistry": AttestationId.AADHAAR, + // "DeployAadhaarRegistryModule#IdentityRegistry": AttestationId.AADHAAR, + "DeployKycRegistryModule#IdentityRegistry": AttestationId.KYC, }; const ids = (() => { @@ -61,6 +63,8 @@ export default buildModule("UpdateHubRegistries", (m) => { const hubContract = updateHubRegistries(m, hubAddress, deployedAddresses); + + return { hubContract, }; diff --git a/contracts/ignition/modules/hub/updateVerifiers.ts b/contracts/ignition/modules/hub/updateVerifiers.ts index 1ceb03ea7..51b197658 100644 --- a/contracts/ignition/modules/hub/updateVerifiers.ts +++ b/contracts/ignition/modules/hub/updateVerifiers.ts @@ -9,6 +9,7 @@ const AttestationId = { E_PASSPORT: "0x0000000000000000000000000000000000000000000000000000000000000001", EU_ID_CARD: "0x0000000000000000000000000000000000000000000000000000000000000002", AADHAAR: "0x0000000000000000000000000000000000000000000000000000000000000003", + KYC: "0x0000000000000000000000000000000000000000000000000000000000000004", }; // Circuit type mappings based on circuit names @@ -21,6 +22,8 @@ const getCircuitType = ( return { attestationId: AttestationId.EU_ID_CARD, typeId, circuitType: "register" }; } else if (circuitName === "register_aadhaar") { return { attestationId: AttestationId.AADHAAR, typeId, circuitType: "register" }; + } else if (circuitName === "register_kyc") { + return { attestationId: AttestationId.KYC, typeId, circuitType: "register" }; } else { return { attestationId: AttestationId.E_PASSPORT, typeId, circuitType: "register" }; } @@ -33,6 +36,8 @@ const getCircuitType = ( return { attestationId: AttestationId.EU_ID_CARD, typeId: 0, circuitType: "vc_and_disclose" }; } else if (circuitName === "vc_and_disclose_aadhaar") { return { attestationId: AttestationId.AADHAAR, typeId: 0, circuitType: "vc_and_disclose" }; + } else if (circuitName === "vc_and_disclose_kyc") { + return { attestationId: AttestationId.KYC, typeId: 0, circuitType: "vc_and_disclose" }; } else { return { attestationId: AttestationId.E_PASSPORT, typeId: 0, circuitType: "vc_and_disclose" }; } diff --git a/contracts/ignition/modules/registry/deployKycRegistry.ts b/contracts/ignition/modules/registry/deployKycRegistry.ts new file mode 100644 index 000000000..4c98e0b47 --- /dev/null +++ b/contracts/ignition/modules/registry/deployKycRegistry.ts @@ -0,0 +1,52 @@ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; +import { artifacts } from "hardhat"; +import { ethers } from "ethers"; + +export default buildModule("DeployKycRegistryModule", (m) => { + // Deploy PoseidonT3 + console.log("📚 Deploying PoseidonT3 library..."); + const poseidonT3 = m.library("PoseidonT3"); + + console.log("🏗️ Deploying IdentityRegistryKycImplV1 implementation..."); + // Deploy IdentityRegistryImplV1 + const identityRegistryKycImpl = m.contract("IdentityRegistryKycImplV1", [], { + libraries: { PoseidonT3: poseidonT3 }, + }); + + console.log("⚙️ Preparing registry initialization data..."); + // Get the interface and encode the initialize function call + const registryInterface = getRegistryInitializeData(); + + const registryInitData = registryInterface.encodeFunctionData("initialize", [ethers.ZeroAddress, ethers.ZeroAddress]); + console.log(" Init data:", registryInitData); + + console.log("🚀 Deploying IdentityRegistry proxy..."); + // Deploy the proxy contract with the implementation address and initialization data + const registry = m.contract("IdentityRegistry", [identityRegistryKycImpl, registryInitData]); + + const gcpKycVerifier = m.contract("Verifier_gcp_jwt", []); + + const pcr0Manager = m.contract("PCR0Manager", []); + + console.log("✅ Registry deployment module setup complete!"); + console.log(" 📋 Summary:"); + console.log(" - PoseidonT3: Library"); + console.log(" - IdentityRegistryKycImplV1: Implementation contract"); + console.log(" - IdentityRegistry: Proxy contract"); + console.log(" - Verifier_gcp_jwt: GCP JWT verifier contract"); + console.log(" - PCR0Manager: PCR0Manager contract"); + + return { + poseidonT3, + identityRegistryKycImpl, + registry, + gcpKycVerifier, + pcr0Manager, + }; +}); + +function getRegistryInitializeData() { + const registryArtifact = artifacts.readArtifactSync("IdentityRegistryKycImplV1"); + const registryInterface = new ethers.Interface(registryArtifact.abi); + return registryInterface; +} diff --git a/contracts/ignition/modules/registry/updateRegistries.ts b/contracts/ignition/modules/registry/updateRegistries.ts index 0cfaaa572..29a89d0c8 100644 --- a/contracts/ignition/modules/registry/updateRegistries.ts +++ b/contracts/ignition/modules/registry/updateRegistries.ts @@ -19,18 +19,29 @@ const registries = { // hub: "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74", // cscaRoot: "13859398115974385161464830211947258005860166431741677064758266112192747818198", // }, - "DeployAadhaarRegistryModule#IdentityRegistry": { + // "DeployAadhaarRegistryModule#IdentityRegistry": { + // shouldChange: true, + // nameAndDobOfac: "4183822562579010781434914867177251983368244626022840551534475857364967864437", + // nameAndYobOfac: "14316795765689804800341464910235935757494922653038299433675973925727164473934", + // hub: "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF", + // pubkeyCommitments: [ + // "5648956411273136337349787488442520720416229937879112788241850936049694492145", + // "18304035373718681408213540837772113004961405604264885188535510276454415833542", + // "3099763118716361008062312602688327679110629275746483297740895929951765195538", + // "5960616419594750988984019912914733527854225713611991429799390436159340745422", + // "1312086597361744268424404341813751658452218312204370523713186983060138886330", + // ], + // }, + "DeployKycRegistryModule#IdentityRegistry": { shouldChange: true, - nameAndDobOfac: "4183822562579010781434914867177251983368244626022840551534475857364967864437", - nameAndYobOfac: "14316795765689804800341464910235935757494922653038299433675973925727164473934", - hub: "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF", - pubkeyCommitments: [ - "5648956411273136337349787488442520720416229937879112788241850936049694492145", - "18304035373718681408213540837772113004961405604264885188535510276454415833542", - "3099763118716361008062312602688327679110629275746483297740895929951765195538", - "5960616419594750988984019912914733527854225713611991429799390436159340745422", - "1312086597361744268424404341813751658452218312204370523713186983060138886330", - ], + hub: "0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74", + nameAndDobOfac: "12056959379782485690824392224737824782985009863971097094085968061978428696483", + nameAndYobOfac: "14482015433179009576094845155298164108788397224633034095648782513909282765564", + onlyTEEAddress: "0xe6b2856a51a17bd4edeb88b3f74370d64475b0fc", + gcpJWTVerifier: "0x13ee8CEa15a262D81a245b37889F7b4bEd015f4c", + pcr0Manager: "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2", + imageDigest: "0x67368d91dc708dee7be8fd9d85eff1fce3181e6e5b9fdfa37fc2d99034ea88e6", + gcpRootCAPubkeyHash: "14165687497759817957828709957846495993787741657460065475757428560999622217191" }, }; @@ -40,6 +51,7 @@ function getImplementationName(registryModule: string): string { "DeployRegistryModule#IdentityRegistry": "IdentityRegistryImplV1", "DeployIdCardRegistryModule#IdentityRegistry": "IdentityRegistryIdCardImplV1", "DeployAadhaarRegistryModule#IdentityRegistry": "IdentityRegistryAadhaarImplV1", + "DeployKycRegistryModule#IdentityRegistry": "IdentityRegistryKycImplV1", }; return implMap[registryModule] || "IdentityRegistryImplV1"; @@ -70,51 +82,79 @@ export function handleRegistryDeployment( let currentOperation: any = registryContract; - if (registryData.shouldChange) { - // Update hub for all registries - if (registryData.hub) { - const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call(registryContract, "updateHub", [registryData.hub], callOptions); - } + if (!registryData.shouldChange) { + return { registryContract, lastOperation: currentOperation }; + } - if (registryData.cscaRoot) { - const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call(registryContract, "updateCscaRoot", [registryData.cscaRoot], callOptions); - } + // Update hub for all registries + if (registryData.hub) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "updateHub", [registryData.hub], callOptions); + } - if (registryData.passportNoOfac) { - const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call( - registryContract, - "updatePassportNoOfacRoot", - [registryData.passportNoOfac], - callOptions, - ); - } - if (registryData.nameAndDobOfac) { - const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call( - registryContract, - "updateNameAndDobOfacRoot", - [registryData.nameAndDobOfac], - callOptions, - ); - } - if (registryData.nameAndYobOfac) { - const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call( - registryContract, - "updateNameAndYobOfacRoot", - [registryData.nameAndYobOfac], - callOptions, - ); - } + if (registryData.cscaRoot) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "updateCscaRoot", [registryData.cscaRoot], callOptions); + } - if (registryData.pubkeyCommitments && registryData.pubkeyCommitments.length > 0) { - for (const pubkeyCommitment of registryData.pubkeyCommitments) { - const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call(registryContract, "registerUidaiPubkeyCommitment", [pubkeyCommitment], callOptions); - } + if (registryData.passportNoOfac) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call( + registryContract, + "updatePassportNoOfacRoot", + [registryData.passportNoOfac], + callOptions, + ); + } + if (registryData.nameAndDobOfac) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call( + registryContract, + "updateNameAndDobOfacRoot", + [registryData.nameAndDobOfac], + callOptions, + ); + } + if (registryData.nameAndYobOfac) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call( + registryContract, + "updateNameAndYobOfacRoot", + [registryData.nameAndYobOfac], + callOptions, + ); + } + + if (registryData.gcpRootCAPubkeyHash) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "updateGCPRootCAPubkeyHash", [registryData.gcpRootCAPubkeyHash], callOptions); + } + + if (registryData.pubkeyCommitments && registryData.pubkeyCommitments.length > 0) { + for (const pubkeyCommitment of registryData.pubkeyCommitments) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "registerUidaiPubkeyCommitment", [pubkeyCommitment], callOptions); + } + } + + if (registryData.onlyTEEAddress) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "updateTEE", [registryData.onlyTEEAddress], callOptions); + } + + if (registryData.gcpJWTVerifier) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "updateGCPJWTVerifier", [registryData.gcpJWTVerifier], callOptions); + } + + if (registryData.pcr0Manager) { + const callOptions = { after: [currentOperation], id: ids() }; + currentOperation = m.call(registryContract, "updatePCR0Manager", [registryData.pcr0Manager], callOptions); + + if (registryData.imageDigest) { + const callOptions = { after: [currentOperation], id: ids() }; + const pcr0Manager = m.contractAt("PCR0Manager", registryData.pcr0Manager); + currentOperation = m.call(pcr0Manager, "addPCR0", [registryData.imageDigest], callOptions); } } diff --git a/contracts/ignition/modules/verifiers/deployAllVerifiers.ts b/contracts/ignition/modules/verifiers/deployAllVerifiers.ts index 3b04995ae..3ee301c18 100644 --- a/contracts/ignition/modules/verifiers/deployAllVerifiers.ts +++ b/contracts/ignition/modules/verifiers/deployAllVerifiers.ts @@ -55,6 +55,7 @@ export type CircuitName = | "register_id_sha512_sha512_sha512_rsa_65537_4096" | "register_id_sha512_sha512_sha512_rsapss_65537_64_2048" | "register_aadhaar" + | "register_kyc" | "register_sha1_sha1_sha1_rsa_64321_4096" | "register_sha256_sha1_sha1_rsa_65537_4096" | "register_sha256_sha256_sha256_rsapss_65537_32_4096" @@ -86,7 +87,8 @@ export type CircuitName = | "dsc_sha256_rsa_56611_4096" | "vc_and_disclose" | "vc_and_disclose_id" - | "vc_and_disclose_aadhaar"; + | "vc_and_disclose_aadhaar" + | "vc_and_disclose_kyc"; // Record mapping circuit names to numbers export const circuitIds: Record = { @@ -148,6 +150,7 @@ export const circuitIds: Record = { register_sha256_sha256_sha256_rsapss_65537_32_4096: [true, 55], register_id_sha512_sha512_sha256_rsapss_65537_32_2048: [true, 56], register_sha512_sha512_sha256_rsapss_65537_32_2048: [true, 57], + register_kyc: [true, 58], dsc_sha1_ecdsa_brainpoolP256r1: [true, 0], dsc_sha1_rsa_65537_4096: [true, 1], @@ -177,6 +180,7 @@ export const circuitIds: Record = { vc_and_disclose: [true, 24], vc_and_disclose_id: [true, 25], vc_and_disclose_aadhaar: [true, 26], + vc_and_disclose_kyc: [true, 27], }; export default buildModule("DeployAllVerifiers", (m) => { From db28b8a13001e53631d4b3eca5d9f647436afc2f Mon Sep 17 00:00:00 2001 From: Javier Cortejoso Date: Mon, 2 Feb 2026 11:46:08 +0100 Subject: [PATCH 09/19] chore: update circuits-build.yml to clarify build timeout and enhance circuit build steps --- .github/workflows/circuits-build.yml | 58 +++++++++++++++++----------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/.github/workflows/circuits-build.yml b/.github/workflows/circuits-build.yml index 178a4d5ce..995a785a6 100644 --- a/.github/workflows/circuits-build.yml +++ b/.github/workflows/circuits-build.yml @@ -33,7 +33,8 @@ concurrency: jobs: build: runs-on: ["128ram"] - timeout-minutes: 720 # 12 hours + # GitHub-hosted runners cap at 360 min (6h); 720 applies if using self-hosted + timeout-minutes: 720 permissions: contents: read actions: read @@ -165,7 +166,7 @@ jobs: path: output/ run_id: ${{ inputs.run-id }} - - name: Build cpp circuits + - name: Prepare build scripts run: | chmod +x circuits/scripts/build/build_cpp.sh chmod +x circuits/scripts/build/build_single_circuit.sh @@ -173,47 +174,58 @@ jobs: # Validate inputs - only one should be provided if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then if [[ "${{ inputs.circuit-type }}" != "" && "${{ inputs.circuit-name }}" != "" ]]; then - echo " Error: Cannot provide both circuit-type and circuit-name. Use only one." + echo "Error: Cannot provide both circuit-type and circuit-name. Use only one." exit 1 fi fi - # Check what type of build to perform - if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.circuit-name }}" != "" ]]; then - # Build circuits by name + - name: Build cpp circuits (workflow_dispatch by name/type) + if: github.event_name == 'workflow_dispatch' && (inputs.circuit-name != '' || inputs.circuit-type != '') + run: | + if [[ "${{ inputs.circuit-name }}" != "" ]]; then INPUT_CIRCUITS="${{ inputs.circuit-name }}" INPUT_CIRCUITS=$(echo "$INPUT_CIRCUITS" | tr -d ' ') IFS=',' read -ra CIRCUITS_ARRAY <<< "$INPUT_CIRCUITS" - - echo "Building selected circuits: ${{ inputs.circuit-name }}" + echo "Building selected circuits by name: ${{ inputs.circuit-name }}" for circuit_name in "${CIRCUITS_ARRAY[@]}"; do echo "Building circuit: $circuit_name" ./circuits/scripts/build/build_single_circuit.sh "$circuit_name" done - - elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.circuit-type }}" != "" ]]; then - # Build circuits by type + else INPUT_CIRCUITS="${{ inputs.circuit-type }}" INPUT_CIRCUITS=$(echo "$INPUT_CIRCUITS" | tr -d ' ') IFS=',' read -ra CIRCUITS_ARRAY <<< "$INPUT_CIRCUITS" - - echo "Building selected circuits: ${{ inputs.circuit-type }}" + echo "Building selected circuits by type: ${{ inputs.circuit-type }}" for circuit in "${CIRCUITS_ARRAY[@]}"; do echo "Building circuit: $circuit" ./circuits/scripts/build/build_cpp.sh "$circuit" done - - else - # Build all circuits (default behavior) - echo "Building all circuits (default behavior)" - ./circuits/scripts/build/build_cpp.sh register - ./circuits/scripts/build/build_cpp.sh register_id - ./circuits/scripts/build/build_cpp.sh register_aadhaar - ./circuits/scripts/build/build_cpp.sh register_kyc - ./circuits/scripts/build/build_cpp.sh disclose - ./circuits/scripts/build/build_cpp.sh dsc fi + - name: Build cpp circuits - register + if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '') + run: ./circuits/scripts/build/build_cpp.sh register + + - name: Build cpp circuits - register_id + if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '') + run: ./circuits/scripts/build/build_cpp.sh register_id + + - name: Build cpp circuits - register_aadhaar + if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '') + run: ./circuits/scripts/build/build_cpp.sh register_aadhaar + + - name: Build cpp circuits - register_kyc + if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '') + run: ./circuits/scripts/build/build_cpp.sh register_kyc + + - name: Build cpp circuits - disclose + if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '') + run: ./circuits/scripts/build/build_cpp.sh disclose + + - name: Build cpp circuits - dsc + if: github.event_name != 'workflow_dispatch' || (inputs.circuit-name == '' && inputs.circuit-type == '') + run: ./circuits/scripts/build/build_cpp.sh dsc + - name: Upload Artifact uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 with: From 0a256e0aaa078e5c3d7ccdf33371b271662231e4 Mon Sep 17 00:00:00 2001 From: Javier Cortejoso Date: Mon, 2 Feb 2026 12:08:16 +0100 Subject: [PATCH 10/19] chore: update circuits-build.yml to change runner specifications for improved build performance --- .github/workflows/circuits-build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/circuits-build.yml b/.github/workflows/circuits-build.yml index 995a785a6..36e545b26 100644 --- a/.github/workflows/circuits-build.yml +++ b/.github/workflows/circuits-build.yml @@ -32,7 +32,10 @@ concurrency: jobs: build: - runs-on: ["128ram"] + runs-on: + - "64ram" + - "self-hosted" + - "selfxyz-org" # GitHub-hosted runners cap at 360 min (6h); 720 applies if using self-hosted timeout-minutes: 720 permissions: From a7b790d41c020db7169742124df16b3ac65617cf Mon Sep 17 00:00:00 2001 From: Javier Cortejoso Date: Mon, 2 Feb 2026 14:54:24 +0100 Subject: [PATCH 11/19] chore: modify circuits-build.yml to reduce runner memory allocation from 64ram to 32ram --- .github/workflows/circuits-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/circuits-build.yml b/.github/workflows/circuits-build.yml index 36e545b26..d7385a826 100644 --- a/.github/workflows/circuits-build.yml +++ b/.github/workflows/circuits-build.yml @@ -33,7 +33,7 @@ concurrency: jobs: build: runs-on: - - "64ram" + - "32ram" - "self-hosted" - "selfxyz-org" # GitHub-hosted runners cap at 360 min (6h); 720 applies if using self-hosted From ebdc639c882a6c076fc27b1e4f30b798d22883cc Mon Sep 17 00:00:00 2001 From: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:00:19 +1000 Subject: [PATCH 12/19] fix: resolve issue from losing connection mid proof verifcation (#1627) * fix: resolve issue from losing connection mid proof verifcation * fix: properly clean up socket resource to prevent memory leak * fix: proper memory leak fix + have both proactive/reactive reconnection * fix: reset scroll state on QR scans and improve WebSocket reconnection handling * chore: yarn fmt --- app/src/screens/verification/ProveScreen.tsx | 14 ++ .../src/proving/provingMachine.ts | 145 +++++++++++++++++- .../src/stores/selfAppStore.tsx | 33 ++-- 3 files changed, 166 insertions(+), 26 deletions(-) diff --git a/app/src/screens/verification/ProveScreen.tsx b/app/src/screens/verification/ProveScreen.tsx index 641d311d2..8f0183b4e 100644 --- a/app/src/screens/verification/ProveScreen.tsx +++ b/app/src/screens/verification/ProveScreen.tsx @@ -172,6 +172,20 @@ const ProveScreen: React.FC = () => { if (selectedAppRef.current?.sessionId !== selectedApp.sessionId) { hasInitializedScrollStateRef.current = false; setHasScrolledToBottom(false); + + // After state reset, check if content is short using current measurements. + // Use setTimeout(0) to ensure we read values AFTER React processes the reset, + // without adding measurements to dependencies (which causes race conditions). + setTimeout(() => { + const hasMeasurements = + scrollViewContentHeight > 0 && scrollViewHeight > 0; + const isShort = scrollViewContentHeight <= scrollViewHeight + 50; + + if (hasMeasurements && isShort) { + setHasScrolledToBottom(true); + hasInitializedScrollStateRef.current = true; + } + }, 0); } setDefaultDocumentTypeIfNeeded(); diff --git a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts index 3b6b1b647..bcf7ec396 100644 --- a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts +++ b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts @@ -211,6 +211,7 @@ export interface ProvingState { sharedKey: Buffer | null; wsConnection: WebSocket | null; wsHandlers: WsHandlers | null; + wsReconnectAttempts: number; socketConnection: Socket | null; uuid: string | null; userConfirmed: boolean; @@ -251,6 +252,7 @@ export interface ProvingState { _handleWsOpen: (selfClient: SelfClient) => void; _handleWsError: (error: Event, selfClient: SelfClient) => void; _handleWsClose: (event: CloseEvent, selfClient: SelfClient) => void; + _reconnectTeeWebSocket: (selfClient: SelfClient) => Promise; _handlePassportNotSupported: (selfClient: SelfClient) => void; _handleAccountRecoveryChoice: (selfClient: SelfClient) => void; @@ -498,6 +500,7 @@ export const useProvingStore = create((set, get) => { sharedKey: null, wsConnection: null, wsHandlers: null, + wsReconnectAttempts: 0, socketConnection: null, uuid: null, userConfirmed: false, @@ -823,6 +826,8 @@ export const useProvingStore = create((set, get) => { reason: event.reason, }); const currentState = get().currentState; + + // Handle unexpected close during active proving states if ( currentState === 'init_tee_connexion' || currentState === 'proving' || @@ -836,11 +841,105 @@ export const useProvingStore = create((set, get) => { selfClient, ); } + + // In ready_to_prove state, attempt automatic reconnection to handle network interruptions. + // Users may lose connectivity briefly; reconnecting transparently improves UX. + if (currentState === 'ready_to_prove') { + const MAX_RECONNECT_ATTEMPTS = 3; + const attempts = get().wsReconnectAttempts; + + if (attempts < MAX_RECONNECT_ATTEMPTS) { + selfClient.logProofEvent('info', 'TEE WebSocket reconnection attempt', context, { + attempt: attempts + 1, + max_attempts: MAX_RECONNECT_ATTEMPTS, + }); + set({ wsConnection: null, wsReconnectAttempts: attempts + 1 }); + + const backoffMs = Math.min(1000 * Math.pow(2, attempts), 10000); + setTimeout(() => { + if (get().currentState === 'ready_to_prove') { + get()._reconnectTeeWebSocket(selfClient); + } + }, backoffMs); + return; + } + + selfClient.logProofEvent('error', 'TEE WebSocket reconnection exhausted', context, { + failure: 'PROOF_FAILED_CONNECTION', + attempts: MAX_RECONNECT_ATTEMPTS, + }); + get()._handleWebSocketMessage( + new MessageEvent('error', { + data: JSON.stringify({ error: 'WebSocket reconnection failed' }), + }), + selfClient, + ); + } + if (get().wsConnection) { set({ wsConnection: null }); } }, + /** + * Re-establishes the TEE WebSocket connection using stored circuit parameters. + * Called automatically when connection is lost in ready_to_prove state. + */ + _reconnectTeeWebSocket: async (selfClient: SelfClient): Promise => { + const context = createProofContext(selfClient, '_reconnectTeeWebSocket'); + const { passportData, circuitType } = get(); + + if (!passportData || !circuitType) { + selfClient.logProofEvent('error', 'Reconnect failed: missing prerequisites', context); + return false; + } + + const typedCircuitType = circuitType as 'disclose' | 'register' | 'dsc'; + const circuitName = + typedCircuitType === 'disclose' + ? passportData.documentCategory === 'aadhaar' + ? 'disclose_aadhaar' + : 'disclose' + : getCircuitNameFromPassportData(passportData, typedCircuitType as 'register' | 'dsc'); + + const wsRpcUrl = resolveWebSocketUrl(selfClient, typedCircuitType, passportData as PassportData, circuitName); + if (!wsRpcUrl) { + selfClient.logProofEvent('error', 'Reconnect failed: no WebSocket URL', context); + return false; + } + + selfClient.logProofEvent('info', 'TEE WebSocket reconnection started', context); + + return new Promise(resolve => { + const ws = new WebSocket(wsRpcUrl); + const RECONNECT_TIMEOUT_MS = 15000; + + const wsHandlers: WsHandlers = { + message: (event: MessageEvent) => get()._handleWebSocketMessage(event, selfClient), + open: () => { + selfClient.logProofEvent('info', 'TEE WebSocket reconnected', context); + set({ wsReconnectAttempts: 0 }); + resolve(true); + }, + error: (error: Event) => get()._handleWsError(error, selfClient), + close: (event: CloseEvent) => get()._handleWsClose(event, selfClient), + }; + + set({ wsConnection: ws, wsHandlers }); + ws.addEventListener('message', wsHandlers.message); + ws.addEventListener('open', wsHandlers.open); + ws.addEventListener('error', wsHandlers.error); + ws.addEventListener('close', wsHandlers.close); + + setTimeout(() => { + if (ws.readyState !== WebSocket.OPEN) { + selfClient.logProofEvent('warn', 'TEE WebSocket reconnection timeout', context); + resolve(false); + } + }, RECONNECT_TIMEOUT_MS); + }); + }, + init: async ( selfClient: SelfClient, circuitType: 'dsc' | 'disclose' | 'register', @@ -1293,7 +1392,7 @@ export const useProvingStore = create((set, get) => { close: (event: CloseEvent) => get()._handleWsClose(event, selfClient), }; - set({ wsConnection: ws, wsHandlers }); + set({ wsConnection: ws, wsHandlers, wsReconnectAttempts: 0 }); ws.addEventListener('message', wsHandlers.message); ws.addEventListener('open', wsHandlers.open); @@ -1318,7 +1417,8 @@ export const useProvingStore = create((set, get) => { startProving: async (selfClient: SelfClient) => { _checkActorInitialized(actor); const startTime = Date.now(); - const { wsConnection, sharedKey, passportData, secret, uuid } = get(); + let { wsConnection } = get(); + const { sharedKey, passportData, secret, uuid } = get(); const context = createProofContext(selfClient, 'startProving', { sessionId: uuid || get().uuid || 'unknown-session', }); @@ -1330,17 +1430,45 @@ export const useProvingStore = create((set, get) => { console.error('Cannot start proving: Not in ready_to_prove state.'); return; } - if (!wsConnection || !sharedKey || !passportData || !secret || !uuid) { + + // Check non-connection prerequisites first + if (!sharedKey || !passportData || !secret || !uuid) { selfClient.logProofEvent('error', 'Missing proving prerequisites', context, { failure: 'PROOF_FAILED_CONNECTION', }); - console.error('Cannot start proving: Missing wsConnection, sharedKey, passportData, secret, or uuid.'); + console.error('Cannot start proving: Missing sharedKey, passportData, secret, or uuid.'); actor!.send({ type: 'PROVE_ERROR' }); return; } + // Attempt reconnection if WebSocket is missing or not open + if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) { + selfClient.logProofEvent('warn', 'WebSocket not ready, attempting reconnection', context, { + wsConnectionExists: !!wsConnection, + readyState: wsConnection?.readyState, + }); + + const reconnected = await get()._reconnectTeeWebSocket(selfClient); + if (!reconnected) { + selfClient.logProofEvent('error', 'WebSocket reconnection failed', context, { + failure: 'PROOF_FAILED_CONNECTION', + }); + actor!.send({ type: 'PROVE_ERROR' }); + return; + } + + // Get the new connection after reconnection + wsConnection = get().wsConnection; + if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) { + selfClient.logProofEvent('error', 'Reconnected WebSocket not ready', context, { + failure: 'PROOF_FAILED_CONNECTION', + }); + actor!.send({ type: 'PROVE_ERROR' }); + return; + } + } + try { - // Emit event for FCM token registration selfClient.emit(SdkEvents.PROVING_BEGIN_GENERATION, { uuid, isMock: passportData?.mock ?? false, @@ -1350,7 +1478,12 @@ export const useProvingStore = create((set, get) => { selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_STARTED); selfClient.logProofEvent('info', 'Payload generation started', context); const submitBody = await get()._generatePayload(selfClient); - wsConnection.send(JSON.stringify(submitBody)); + + const activeWsConnection = get().wsConnection; + if (!activeWsConnection) { + throw new Error('WebSocket connection lost during payload generation'); + } + activeWsConnection.send(JSON.stringify(submitBody)); selfClient.logProofEvent('info', 'Payload sent over WebSocket', context); selfClient.trackEvent(ProofEvents.PAYLOAD_SENT); selfClient.trackEvent(ProofEvents.PROVING_PROCESS_STARTED); diff --git a/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx b/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx index 5af1f350e..613c53f67 100644 --- a/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx +++ b/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx @@ -13,8 +13,6 @@ import { WS_DB_RELAYER } from '@selfxyz/common'; * Zustand state backing the in-app handoff between the SDK and the hosted Self * application. The store tracks the active websocket session, latest * {@link SelfApp} payload, and helper callbacks used by the proving machine. - * Consumers should treat the state as ephemeral and expect it to reset whenever - * the socket disconnects. */ export interface SelfAppState { selfApp: SelfApp | null; @@ -80,24 +78,17 @@ export const useSelfAppStore = create((set, get) => ({ socket.on('connect', () => {}); - // Listen for the event only once per connection attempt socket.once('self_app', (data: unknown) => { try { const appData: SelfApp = typeof data === 'string' ? JSON.parse(data) : (data as SelfApp); - // Basic validation if (!appData || typeof appData !== 'object' || !appData.sessionId) { - console.error('[SelfAppStore] Invalid app data received:', appData); - // Optionally clear the app data or handle the error appropriately + console.error('[SelfAppStore] Invalid app data received'); set({ selfApp: null }); return; } if (appData.sessionId !== get().sessionId) { - console.warn( - `[SelfAppStore] Received SelfApp for session ${ - appData.sessionId - }, but current session is ${get().sessionId}. Ignoring.`, - ); + console.warn('[SelfAppStore] Session mismatch, ignoring payload'); return; } @@ -109,20 +100,22 @@ export const useSelfAppStore = create((set, get) => ({ }); socket.on('connect_error', error => { - console.error('[SelfAppStore] Mobile WS connection error:', error); - // Clean up on connection error - get().cleanSelfApp(); + // Socket.io handles reconnection automatically with exponential backoff. + // State is preserved to allow seamless recovery when network returns. + console.error('[SelfAppStore] Connection error:', error.message); }); socket.on('error', error => { - console.error('[SelfAppStore] Mobile WS error:', error); - // Consider if cleanup is needed here as well + console.error('[SelfAppStore] Socket error:', error); }); - socket.on('disconnect', (_reason: string) => { - // Prevent cleaning up if disconnect was initiated by cleanSelfApp - if (get().socket === socket) { - set({ socket: null, sessionId: null, selfApp: null }); + socket.on('disconnect', (reason: string) => { + if (get().socket !== socket) return; + + // Only clear state on intentional disconnects. For transient network issues + // (transport close, ping timeout), socket.io reconnects automatically. + if (reason === 'io server disconnect' || reason === 'io client disconnect') { + set({ socket: null, sessionId: null }); } }); } catch (error) { From 8b87ba36ab5089ffcf1d4385a71086ba64024b72 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Mon, 2 Feb 2026 16:34:08 -0800 Subject: [PATCH 13/19] [SELF-1952] UI: Create KYC verified screen; prompt to enter proving flow (#1681) * first pass at kyc verified screen * finalize kyc verified design * add queue buffer --- app/src/navigation/index.tsx | 7 + app/src/navigation/onboarding.ts | 8 + .../notificationTrackingProvider.tsx | 100 ++++ app/src/screens/kyc/KYCVerifiedScreen.tsx | 83 +++ app/tests/src/navigation.test.tsx | 1 + .../notificationTrackingProvider.test.tsx | 511 ++++++++++++++++++ .../screens/kyc/KYCVerifiedScreen.test.tsx | 156 ++++++ 7 files changed, 866 insertions(+) create mode 100644 app/src/screens/kyc/KYCVerifiedScreen.tsx create mode 100644 app/tests/src/providers/notificationTrackingProvider.test.tsx create mode 100644 app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index d7dfb4ed5..76fa3a846 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -80,6 +80,7 @@ export type RootStackParamList = Omit< | 'IDPicker' | 'IdDetails' | 'KycSuccess' + | 'KYCVerified' | 'RegistrationFallback' | 'Loading' | 'Modal' @@ -207,6 +208,12 @@ export type RootStackParamList = Omit< userId?: string; } | undefined; + KYCVerified: + | { + status?: string; + userId?: string; + } + | undefined; // Dev screens CreateMock: undefined; diff --git a/app/src/navigation/onboarding.ts b/app/src/navigation/onboarding.ts index 72156f420..bf5d4a769 100644 --- a/app/src/navigation/onboarding.ts +++ b/app/src/navigation/onboarding.ts @@ -5,6 +5,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen'; +import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen'; import AccountVerifiedSuccessScreen from '@/screens/onboarding/AccountVerifiedSuccessScreen'; import DisclaimerScreen from '@/screens/onboarding/DisclaimerScreen'; import SaveRecoveryPhraseScreen from '@/screens/onboarding/SaveRecoveryPhraseScreen'; @@ -38,6 +39,13 @@ const onboardingScreens = { animation: 'slide_from_bottom', } as NativeStackNavigationOptions, }, + KYCVerified: { + screen: KYCVerifiedScreen, + options: { + headerShown: false, + animation: 'slide_from_bottom', + } as NativeStackNavigationOptions, + }, }; export default onboardingScreens; diff --git a/app/src/providers/notificationTrackingProvider.tsx b/app/src/providers/notificationTrackingProvider.tsx index a4346c8e1..5462f291c 100644 --- a/app/src/providers/notificationTrackingProvider.tsx +++ b/app/src/providers/notificationTrackingProvider.tsx @@ -4,16 +4,89 @@ import type { PropsWithChildren } from 'react'; import React, { useEffect } from 'react'; +import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import messaging from '@react-native-firebase/messaging'; import { NotificationEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { navigationRef } from '@/navigation'; import { trackEvent } from '@/services/analytics'; +// Queue for pending navigation actions that need to wait for navigation to be ready +let pendingNavigation: FirebaseMessagingTypes.RemoteMessage | null = null; + +/** + * Execute navigation for a notification + * @returns true if navigation was executed, false if it needs to be queued + */ +const executeNotificationNavigation = ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, +): boolean => { + if (!navigationRef.isReady()) { + return false; + } + + const notificationType = remoteMessage.data?.type; + const status = remoteMessage.data?.status; + + // Handle KYC result notifications + if (notificationType === 'kyc_result' && status === 'approved') { + navigationRef.navigate('KYCVerified', { + status: String(status), + userId: remoteMessage.data?.user_id + ? String(remoteMessage.data.user_id) + : undefined, + }); + return true; + } + // Add handling for other notification types here as needed + // For retry/rejected statuses, could navigate to appropriate screens in future + + return true; // Navigation handled (or not applicable) +}; + +/** + * Handle navigation based on notification type and data + * Queues navigation if navigationRef is not ready yet + */ +const handleNotificationNavigation = ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, +) => { + const executed = executeNotificationNavigation(remoteMessage); + + if (!executed) { + // Navigation not ready yet - queue for later + pendingNavigation = remoteMessage; + if (__DEV__) { + console.log( + 'Navigation not ready, queuing notification navigation:', + remoteMessage.data?.type, + ); + } + } +}; + +/** + * Process any pending navigation once navigation is ready + */ +const processPendingNavigation = () => { + if (pendingNavigation && navigationRef.isReady()) { + if (__DEV__) { + console.log( + 'Processing pending notification navigation:', + pendingNavigation.data?.type, + ); + } + executeNotificationNavigation(pendingNavigation); + pendingNavigation = null; + } +}; + export const NotificationTrackingProvider: React.FC = ({ children, }) => { useEffect(() => { + // Handle notification tap when app is in background const unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => { trackEvent(NotificationEvents.BACKGROUND_NOTIFICATION_OPENED, { messageId: remoteMessage.messageId, @@ -22,8 +95,12 @@ export const NotificationTrackingProvider: React.FC = ({ // Track if user interacted with any actions actionId: remoteMessage.data?.actionId, }); + + // Handle navigation based on notification type + handleNotificationNavigation(remoteMessage); }); + // Handle notification tap when app is completely closed (cold start) messaging() .getInitialNotification() .then(remoteMessage => { @@ -35,11 +112,34 @@ export const NotificationTrackingProvider: React.FC = ({ // Track if user interacted with any actions actionId: remoteMessage.data?.actionId, }); + + // Handle navigation based on notification type + handleNotificationNavigation(remoteMessage); } }); return unsubscribe; }, []); + // Monitor navigation readiness and process pending navigation + useEffect(() => { + // Check immediately if navigation is already ready + if (navigationRef.isReady()) { + processPendingNavigation(); + return; + } + + // Poll for navigation readiness if not ready yet + const checkInterval = setInterval(() => { + if (navigationRef.isReady()) { + processPendingNavigation(); + clearInterval(checkInterval); + } + }, 100); // Check every 100ms + + // Cleanup interval on unmount + return () => clearInterval(checkInterval); + }, []); + return <>{children}; }; diff --git a/app/src/screens/kyc/KYCVerifiedScreen.tsx b/app/src/screens/kyc/KYCVerifiedScreen.tsx new file mode 100644 index 000000000..f8d71fbde --- /dev/null +++ b/app/src/screens/kyc/KYCVerifiedScreen.tsx @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { + AbstractButton, + Description, + Title, +} from '@selfxyz/mobile-sdk-alpha/components'; +import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors'; + +import { buttonTap } from '@/integrations/haptics'; +import type { RootStackParamList } from '@/navigation'; + +const KYCVerifiedScreen: React.FC = () => { + const navigation = + useNavigation>(); + const insets = useSafeAreaInsets(); + + const handleGenerateProof = () => { + buttonTap(); + navigation.navigate('ProvingScreenRouter'); + }; + + return ( + + + + Your ID has been verified + + Next Self will generate a zk proof specifically for this device that + you can use to proof your identity. + + + + + Generate proof + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: black, + }, + spacer: { + flex: 1, + }, + title: { + color: white, + textAlign: 'center', + fontSize: 28, + letterSpacing: 1, + }, + description: { + color: white, + textAlign: 'center', + fontSize: 18, + lineHeight: 27, + }, +}); + +export default KYCVerifiedScreen; diff --git a/app/tests/src/navigation.test.tsx b/app/tests/src/navigation.test.tsx index e32735297..a46550ba5 100644 --- a/app/tests/src/navigation.test.tsx +++ b/app/tests/src/navigation.test.tsx @@ -85,6 +85,7 @@ describe('navigation', () => { 'Home', 'IDPicker', 'IdDetails', + 'KYCVerified', 'KycSuccess', 'Loading', 'ManageDocuments', diff --git a/app/tests/src/providers/notificationTrackingProvider.test.tsx b/app/tests/src/providers/notificationTrackingProvider.test.tsx new file mode 100644 index 000000000..84f1e43b9 --- /dev/null +++ b/app/tests/src/providers/notificationTrackingProvider.test.tsx @@ -0,0 +1,511 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; +import { render, waitFor } from '@testing-library/react-native'; + +import { navigationRef } from '@/navigation'; +import { NotificationTrackingProvider } from '@/providers/notificationTrackingProvider'; +import * as analytics from '@/services/analytics'; + +// Mock Firebase messaging +const mockOnNotificationOpenedApp = jest.fn(); +const mockGetInitialNotification = jest.fn(); + +jest.mock('@react-native-firebase/messaging', () => { + return jest.fn(() => ({ + onNotificationOpenedApp: mockOnNotificationOpenedApp, + getInitialNotification: mockGetInitialNotification, + })); +}); + +// Mock navigation +jest.mock('@/navigation', () => ({ + navigationRef: { + isReady: jest.fn(), + navigate: jest.fn(), + }, +})); + +// Mock analytics +jest.mock('@/services/analytics', () => ({ + trackEvent: jest.fn(), +})); + +// Mock analytics constants +jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({ + NotificationEvents: { + BACKGROUND_NOTIFICATION_OPENED: 'BACKGROUND_NOTIFICATION_OPENED', + COLD_START_NOTIFICATION_OPENED: 'COLD_START_NOTIFICATION_OPENED', + }, +})); + +const mockNavigationRef = navigationRef as jest.Mocked; + +describe('NotificationTrackingProvider', () => { + const mockUserId = '19f21362-856a-4606-88e1-fa306036978f'; + + beforeEach(() => { + jest.clearAllMocks(); + mockNavigationRef.isReady.mockReturnValue(true); + }); + + it('should render children without errors', () => { + mockGetInitialNotification.mockResolvedValue(null); + + const { getByTestId } = render( + + Test Child + , + ); + + expect(getByTestId('child')).toHaveTextContent('Test Child'); + }); + + describe('Background notification (onNotificationOpenedApp)', () => { + it('should navigate to KYCVerified when notification type is kyc_result and status is approved', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); // Return unsubscribe function + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + expect(mockOnNotificationOpenedApp).toHaveBeenCalled(); + + // Simulate notification tap + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'BACKGROUND_NOTIFICATION_OPENED', + { + messageId: 'test-message-id', + type: 'kyc_result', + actionId: undefined, + }, + ); + + expect(mockNavigationRef.navigate).toHaveBeenCalledWith('KYCVerified', { + status: 'approved', + userId: mockUserId, + }); + }); + }); + + it('should not navigate when status is retry', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'retry', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate for retry status + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate when status is rejected', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'rejected', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate for rejected status + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should handle missing notification data gracefully', async () => { + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: undefined, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate when data is missing + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate when navigation is not ready', async () => { + mockNavigationRef.isReady.mockReturnValue(false); + + let notificationHandler: + | ((message: FirebaseMessagingTypes.RemoteMessage) => void) + | null = null; + + mockOnNotificationOpenedApp.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + if (notificationHandler) { + notificationHandler(remoteMessage); + } + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalled(); + }); + + // Should not navigate when navigationRef is not ready + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('Cold start notification (getInitialNotification)', () => { + it('should navigate to KYCVerified when notification type is kyc_result and status is approved', async () => { + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + { + messageId: 'test-message-id', + type: 'kyc_result', + actionId: undefined, + }, + ); + + expect(mockNavigationRef.navigate).toHaveBeenCalledWith('KYCVerified', { + status: 'approved', + userId: mockUserId, + }); + }); + }); + + it('should not navigate when getInitialNotification returns null', async () => { + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + mockGetInitialNotification.mockResolvedValue(null); + + render( + + Test + , + ); + + await waitFor(() => { + expect(mockGetInitialNotification).toHaveBeenCalled(); + }); + + // Should not track or navigate when there's no initial notification + expect(analytics.trackEvent).not.toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate when status is retry on cold start', async () => { + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'retry', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Should not navigate for retry status + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + + it('should queue navigation when navigationRef is not ready on cold start', async () => { + // Start with navigation not ready + mockNavigationRef.isReady.mockReturnValue(false); + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + // Wait for initial notification to be processed + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Navigation should not have been called yet + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + + // Simulate navigation becoming ready + mockNavigationRef.isReady.mockReturnValue(true); + + // Wait for the polling interval to detect navigation is ready + await waitFor( + () => { + expect(mockNavigationRef.navigate).toHaveBeenCalledWith( + 'KYCVerified', + { + status: 'approved', + userId: mockUserId, + }, + ); + }, + { timeout: 2000 }, + ); + }); + + it('should process pending navigation immediately if navigation becomes ready', async () => { + // Start with navigation not ready + mockNavigationRef.isReady.mockReturnValue(false); + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'kyc_result', + status: 'approved', + user_id: mockUserId, + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + const { rerender } = render( + + Test + , + ); + + // Wait for initial notification to be processed + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Navigation should not have been called yet + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + + // Make navigation ready + mockNavigationRef.isReady.mockReturnValue(true); + + // Trigger a re-render to simulate React's update cycle + rerender( + + Test + , + ); + + // Navigation should be called after navigation becomes ready + await waitFor( + () => { + expect(mockNavigationRef.navigate).toHaveBeenCalledWith( + 'KYCVerified', + { + status: 'approved', + userId: mockUserId, + }, + ); + }, + { timeout: 2000 }, + ); + }); + + it('should not queue navigation for non-KYC notifications when navigation is not ready', async () => { + mockNavigationRef.isReady.mockReturnValue(false); + mockOnNotificationOpenedApp.mockReturnValue(jest.fn()); + + const remoteMessage = { + messageId: 'test-message-id', + data: { + type: 'other_notification', + status: 'some_status', + }, + } as FirebaseMessagingTypes.RemoteMessage; + + mockGetInitialNotification.mockResolvedValue(remoteMessage); + + render( + + Test + , + ); + + await waitFor(() => { + expect(analytics.trackEvent).toHaveBeenCalledWith( + 'COLD_START_NOTIFICATION_OPENED', + expect.anything(), + ); + }); + + // Make navigation ready + mockNavigationRef.isReady.mockReturnValue(true); + + // Wait a bit to ensure no navigation happens + await new Promise(resolve => setTimeout(resolve, 300)); + + // Should not navigate for non-KYC notifications + expect(mockNavigationRef.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx b/app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx new file mode 100644 index 000000000..e28b85888 --- /dev/null +++ b/app/tests/src/screens/kyc/KYCVerifiedScreen.test.tsx @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { fireEvent, render } from '@testing-library/react-native'; + +import * as haptics from '@/integrations/haptics'; +import KYCVerifiedScreen from '@/screens/kyc/KYCVerifiedScreen'; + +// Note: While jest.setup.js provides comprehensive React Native mocking, +// react-test-renderer requires component-based mocks (functions) rather than +// string-based mocks for proper rendering. This minimal mock provides the +// specific components needed for this test without using requireActual to +// avoid memory issues (see .cursor/rules/test-memory-optimization.mdc). +jest.mock('react-native', () => ({ + __esModule: true, + Platform: { OS: 'ios', select: jest.fn() }, + StyleSheet: { + create: (styles: any) => styles, + flatten: (style: any) => style, + }, + View: ({ children, ...props }: any) =>
{children}
, + Text: ({ children, ...props }: any) => {children}, +})); + +jest.mock('react-native-edge-to-edge', () => ({ + SystemBars: () => null, +})); + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0 })), +})); + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), +})); + +// Mock Tamagui components +jest.mock('tamagui', () => ({ + __esModule: true, + YStack: ({ children, ...props }: any) =>
{children}
, + View: ({ children, ...props }: any) =>
{children}
, + Text: ({ children, ...props }: any) => {children}, +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({ + black: '#000000', + white: '#FFFFFF', +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({ + AbstractButton: ({ children, onPress, testID, ...props }: any) => ( + + ), + Title: ({ children, style, testID, ...props }: any) => ( +
+ {children} +
+ ), + Description: ({ children, style, testID, ...props }: any) => ( +
+ {children} +
+ ), +})); + +jest.mock('@/integrations/haptics', () => ({ + buttonTap: jest.fn(), +})); + +jest.mock('@/config/sentry', () => ({ + captureException: jest.fn(), +})); + +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; + +describe('KYCVerifiedScreen', () => { + const mockNavigate = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseNavigation.mockReturnValue({ + navigate: mockNavigate, + } as any); + }); + + it('should render the screen without errors', () => { + const { root } = render(); + expect(root).toBeTruthy(); + }); + + it('should display the correct title', () => { + const { root } = render(); + // Title is the first div child + const titleElement = root.findAll( + node => + node.type === 'div' && + node.props.children === 'Your ID has been verified', + )[0]; + expect(titleElement).toBeTruthy(); + }); + + it('should display the correct description text', () => { + const { root } = render(); + // Description is a div with the description text + const descriptionElement = root.findAll( + node => + node.type === 'div' && + node.props.children === + 'Next Self will generate a zk proof specifically for this device that you can use to proof your identity.', + )[0]; + expect(descriptionElement).toBeTruthy(); + }); + + it('should have a "Generate proof" button that is visible', () => { + const { root } = render(); + const buttons = root.findAllByType('button'); + expect(buttons.length).toBeGreaterThan(0); + expect(buttons[0].props.children).toBe('Generate proof'); + }); + + it('should trigger haptic feedback when "Generate proof" is pressed', () => { + const { root } = render(); + const button = root.findAllByType('button')[0]; + + fireEvent.press(button); + + expect(haptics.buttonTap).toHaveBeenCalledTimes(1); + }); + + it('should navigate to ProvingScreenRouter when "Generate proof" is pressed', () => { + const { root } = render(); + const button = root.findAllByType('button')[0]; + + fireEvent.press(button); + + expect(mockNavigate).toHaveBeenCalledWith('ProvingScreenRouter'); + }); + + it('should have navigation available', () => { + render(); + expect(mockUseNavigation).toHaveBeenCalled(); + }); +}); From fa83f4a14f84f2a7f6cad30c457585a7d96c261f Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Mon, 2 Feb 2026 19:19:15 -0800 Subject: [PATCH 14/19] formatting (#1683) --- .../disclose/Verifier_vc_and_disclose_kyc.sol | 37 +++++++++---------- .../verifiers/gcp/Verifier_gcp_jwt.sol | 37 +++++++++---------- .../register_kyc/Verifier_register_kyc.sol | 37 +++++++++---------- .../ignition/modules/hub/updateRegistries.ts | 2 - .../modules/registry/updateRegistries.ts | 30 +++++---------- 5 files changed, 64 insertions(+), 79 deletions(-) diff --git a/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol b/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol index b8de45e2c..cf4a82547 100644 --- a/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol +++ b/contracts/contracts/verifiers/disclose/Verifier_vc_and_disclose_kyc.sol @@ -22,17 +22,17 @@ pragma solidity >=0.7.0 <0.9.0; contract Groth16Verifier { // Scalar field size - uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Base field size - uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // Verification Key data - uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; - uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; - uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; - uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; - uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; - uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; @@ -42,7 +42,6 @@ contract Groth16Verifier { uint256 constant deltay1 = 8725996148009629609617423651062395041554350094385944632997372312828608644955; uint256 constant deltay2 = 19505227144542990355285832777856832082655385455315296491381347497982380087331; - uint256 constant IC0x = 16649376790350306128495410672000438222835355361873864679185308928608342391377; uint256 constant IC0y = 1365830659239397567654193478106544803466926587095831397836882385286292210457; @@ -133,14 +132,18 @@ contract Groth16Verifier { uint256 constant IC29x = 1170885743391113947515531032472753161485583617637753865725092942330476093342; uint256 constant IC29y = 19204742121781488340297839383055704899252648836617466985181418250802660585322; - // Memory data uint16 constant pVk = 0; uint16 constant pPairing = 128; uint16 constant pLastMem = 896; - function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[29] calldata _pubSignals) public view returns (bool) { + function verifyProof( + uint[2] calldata _pA, + uint[2][2] calldata _pB, + uint[2] calldata _pC, + uint[29] calldata _pubSignals + ) public view returns (bool) { assembly { function checkField(v) { if iszero(lt(v, r)) { @@ -242,7 +245,6 @@ contract Groth16Verifier { g1_mulAccC(_pVk, IC29x, IC29y, calldataload(add(pubSignals, 896))) - // -A mstore(_pPairing, calldataload(pA)) mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) @@ -267,7 +269,6 @@ contract Groth16Verifier { mstore(add(_pPairing, 384), mload(add(pMem, pVk))) mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) - // gamma2 mstore(add(_pPairing, 448), gammax1) mstore(add(_pPairing, 480), gammax2) @@ -284,7 +285,6 @@ contract Groth16Verifier { mstore(add(_pPairing, 704), deltay1) mstore(add(_pPairing, 736), deltay2) - let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) isOk := and(success, mload(_pPairing)) @@ -353,12 +353,11 @@ contract Groth16Verifier { checkField(calldataload(add(_pubSignals, 896))) - // Validate all evaluations let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) mstore(0, isValid) - return(0, 0x20) - } - } - } + return(0, 0x20) + } + } +} diff --git a/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol b/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol index 4d36ea61c..629ddd7a7 100644 --- a/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol +++ b/contracts/contracts/verifiers/gcp/Verifier_gcp_jwt.sol @@ -22,17 +22,17 @@ pragma solidity >=0.7.0 <0.9.0; contract Verifier_gcp_jwt { // Scalar field size - uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Base field size - uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // Verification Key data - uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; - uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; - uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; - uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; - uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; - uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; @@ -42,7 +42,6 @@ contract Verifier_gcp_jwt { uint256 constant deltay1 = 4195707504005103778106485021796359604414786496137920116128130440872062477216; uint256 constant deltay2 = 20513207510859042996645896574478474889840017920990203652675014165180462273668; - uint256 constant IC0x = 6972951741762339913362267428319005943611938060812676091174501911982947323821; uint256 constant IC0y = 4968121098705797351946375443564156998686441710551907423285338106315203657372; @@ -106,14 +105,18 @@ contract Verifier_gcp_jwt { uint256 constant IC20x = 13029408846315391045768292892963336300734709802776968717851605403617397448869; uint256 constant IC20y = 21441391199269244274037661931659719640029973634066921385003370500690694569608; - // Memory data uint16 constant pVk = 0; uint16 constant pPairing = 128; uint16 constant pLastMem = 896; - function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[20] calldata _pubSignals) public view returns (bool) { + function verifyProof( + uint[2] calldata _pA, + uint[2][2] calldata _pB, + uint[2] calldata _pC, + uint[20] calldata _pubSignals + ) public view returns (bool) { assembly { function checkField(v) { if iszero(lt(v, r)) { @@ -197,7 +200,6 @@ contract Verifier_gcp_jwt { g1_mulAccC(_pVk, IC20x, IC20y, calldataload(add(pubSignals, 608))) - // -A mstore(_pPairing, calldataload(pA)) mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) @@ -222,7 +224,6 @@ contract Verifier_gcp_jwt { mstore(add(_pPairing, 384), mload(add(pMem, pVk))) mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) - // gamma2 mstore(add(_pPairing, 448), gammax1) mstore(add(_pPairing, 480), gammax2) @@ -239,7 +240,6 @@ contract Verifier_gcp_jwt { mstore(add(_pPairing, 704), deltay1) mstore(add(_pPairing, 736), deltay2) - let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) isOk := and(success, mload(_pPairing)) @@ -290,12 +290,11 @@ contract Verifier_gcp_jwt { checkField(calldataload(add(_pubSignals, 608))) - // Validate all evaluations let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) mstore(0, isValid) - return(0, 0x20) - } - } - } + return(0, 0x20) + } + } +} diff --git a/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol b/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol index 263f6efd9..5665feac8 100644 --- a/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol +++ b/contracts/contracts/verifiers/register_kyc/Verifier_register_kyc.sol @@ -22,17 +22,17 @@ pragma solidity >=0.7.0 <0.9.0; contract Verifier_register_kyc { // Scalar field size - uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + uint256 constant r = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Base field size - uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + uint256 constant q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // Verification Key data - uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; - uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; - uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; - uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; - uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; - uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; + uint256 constant alphax = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant alphay = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + uint256 constant betax1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant betax2 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant betay1 = 21847035105528745403288232691147584728191162732299865338377159692350059136679; + uint256 constant betay2 = 10505242626370262277552901082094356697409835680220590971873171140371331206856; uint256 constant gammax1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; @@ -42,7 +42,6 @@ contract Verifier_register_kyc { uint256 constant deltay1 = 10401404284625717188368140886450294801087446278285114268746933223843924747393; uint256 constant deltay2 = 21623976071772575613470418289568781837131470676146510317928308200173145329920; - uint256 constant IC0x = 3168135977548073774669686196671110956985263260631963004209946350111009871783; uint256 constant IC0y = 19251271161827058925074199219712324559154387560340229136388386911360884273664; @@ -58,14 +57,18 @@ contract Verifier_register_kyc { uint256 constant IC4x = 11598465374717791235735036209864180918816853983932860910077820062417244512066; uint256 constant IC4y = 10915386471964999341016166937952548568058036159601535214565672698374193076432; - // Memory data uint16 constant pVk = 0; uint16 constant pPairing = 128; uint16 constant pLastMem = 896; - function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) public view returns (bool) { + function verifyProof( + uint[2] calldata _pA, + uint[2][2] calldata _pB, + uint[2] calldata _pC, + uint[4] calldata _pubSignals + ) public view returns (bool) { assembly { function checkField(v) { if iszero(lt(v, r)) { @@ -117,7 +120,6 @@ contract Verifier_register_kyc { g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96))) - // -A mstore(_pPairing, calldataload(pA)) mstore(add(_pPairing, 32), mod(sub(q, calldataload(add(pA, 32))), q)) @@ -142,7 +144,6 @@ contract Verifier_register_kyc { mstore(add(_pPairing, 384), mload(add(pMem, pVk))) mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32)))) - // gamma2 mstore(add(_pPairing, 448), gammax1) mstore(add(_pPairing, 480), gammax2) @@ -159,7 +160,6 @@ contract Verifier_register_kyc { mstore(add(_pPairing, 704), deltay1) mstore(add(_pPairing, 736), deltay2) - let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20) isOk := and(success, mload(_pPairing)) @@ -178,12 +178,11 @@ contract Verifier_register_kyc { checkField(calldataload(add(_pubSignals, 96))) - // Validate all evaluations let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem) mstore(0, isValid) - return(0, 0x20) - } - } - } + return(0, 0x20) + } + } +} diff --git a/contracts/ignition/modules/hub/updateRegistries.ts b/contracts/ignition/modules/hub/updateRegistries.ts index 29754b337..032794a71 100644 --- a/contracts/ignition/modules/hub/updateRegistries.ts +++ b/contracts/ignition/modules/hub/updateRegistries.ts @@ -63,8 +63,6 @@ export default buildModule("UpdateHubRegistries", (m) => { const hubContract = updateHubRegistries(m, hubAddress, deployedAddresses); - - return { hubContract, }; diff --git a/contracts/ignition/modules/registry/updateRegistries.ts b/contracts/ignition/modules/registry/updateRegistries.ts index 29a89d0c8..c6de52a80 100644 --- a/contracts/ignition/modules/registry/updateRegistries.ts +++ b/contracts/ignition/modules/registry/updateRegistries.ts @@ -41,7 +41,7 @@ const registries = { gcpJWTVerifier: "0x13ee8CEa15a262D81a245b37889F7b4bEd015f4c", pcr0Manager: "0xf2810D5E9938816D42F0Ae69D33F013a23C0aED2", imageDigest: "0x67368d91dc708dee7be8fd9d85eff1fce3181e6e5b9fdfa37fc2d99034ea88e6", - gcpRootCAPubkeyHash: "14165687497759817957828709957846495993787741657460065475757428560999622217191" + gcpRootCAPubkeyHash: "14165687497759817957828709957846495993787741657460065475757428560999622217191", }, }; @@ -99,35 +99,25 @@ export function handleRegistryDeployment( if (registryData.passportNoOfac) { const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call( - registryContract, - "updatePassportNoOfacRoot", - [registryData.passportNoOfac], - callOptions, - ); + currentOperation = m.call(registryContract, "updatePassportNoOfacRoot", [registryData.passportNoOfac], callOptions); } if (registryData.nameAndDobOfac) { const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call( - registryContract, - "updateNameAndDobOfacRoot", - [registryData.nameAndDobOfac], - callOptions, - ); + currentOperation = m.call(registryContract, "updateNameAndDobOfacRoot", [registryData.nameAndDobOfac], callOptions); } if (registryData.nameAndYobOfac) { const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call( - registryContract, - "updateNameAndYobOfacRoot", - [registryData.nameAndYobOfac], - callOptions, - ); + currentOperation = m.call(registryContract, "updateNameAndYobOfacRoot", [registryData.nameAndYobOfac], callOptions); } if (registryData.gcpRootCAPubkeyHash) { const callOptions = { after: [currentOperation], id: ids() }; - currentOperation = m.call(registryContract, "updateGCPRootCAPubkeyHash", [registryData.gcpRootCAPubkeyHash], callOptions); + currentOperation = m.call( + registryContract, + "updateGCPRootCAPubkeyHash", + [registryData.gcpRootCAPubkeyHash], + callOptions, + ); } if (registryData.pubkeyCommitments && registryData.pubkeyCommitments.length > 0) { From 2fd8d181074b3f6db4686736e925c610536a1097 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:30:40 +0530 Subject: [PATCH 15/19] ci: build 1 circuit at a time (#1684) --- circuits/scripts/build/build_cpp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuits/scripts/build/build_cpp.sh b/circuits/scripts/build/build_cpp.sh index 29a0eaf90..b30093d8b 100755 --- a/circuits/scripts/build/build_cpp.sh +++ b/circuits/scripts/build/build_cpp.sh @@ -156,7 +156,7 @@ for item in "${allowed_circuits[@]}"; do continue fi - while [[ ${#pids[@]} -ge 2 ]]; do + while [[ ${#pids[@]} -ge 1 ]]; do new_pids=() for pid in "${pids[@]}"; do if kill -0 "$pid" 2>/dev/null; then From 2ebf7918c7a90f86a74c7588121e4cde29b6a36d Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Tue, 3 Feb 2026 12:44:34 +0100 Subject: [PATCH 16/19] chore: self UUID namespace as a const (#1685) --- app/env.ts | 1 - app/src/screens/kyc/KycSuccessScreen.tsx | 4 ++-- app/src/services/notifications/notificationService.ts | 7 ++----- app/tests/src/screens/kyc/KycSuccessScreen.test.tsx | 6 ++---- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/env.ts b/app/env.ts index 52eacf506..5ee1d7dad 100644 --- a/app/env.ts +++ b/app/env.ts @@ -27,7 +27,6 @@ export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true'; export const MIXPANEL_NFC_PROJECT_TOKEN = undefined; export const SEGMENT_KEY = process.env.SEGMENT_KEY; -export const SELF_UUID_NAMESPACE = process.env.SELF_UUID_NAMESPACE; export const SENTRY_DSN = process.env.SENTRY_DSN; export const SUMSUB_TEE_URL = process.env.SUMSUB_TEE_URL || 'http://localhost:8080'; diff --git a/app/src/screens/kyc/KycSuccessScreen.tsx b/app/src/screens/kyc/KycSuccessScreen.tsx index 3269da141..97c55f5a2 100644 --- a/app/src/screens/kyc/KycSuccessScreen.tsx +++ b/app/src/screens/kyc/KycSuccessScreen.tsx @@ -25,9 +25,9 @@ import { buttonTap } from '@/integrations/haptics'; import type { RootStackParamList } from '@/navigation'; import { getFCMToken, - getSelfUuidNamespace, registerDeviceToken, requestNotificationPermission, + SELF_UUID_NAMESPACE, } from '@/services/notifications/notificationService'; import { useSettingStore } from '@/stores/settingStore'; @@ -58,7 +58,7 @@ const KycSuccessScreen: React.FC = ({ setFcmToken(token); trackEvent(ProofEvents.FCM_TOKEN_STORED); - const sessionId = uuidv5(userId, getSelfUuidNamespace()); + const sessionId = uuidv5(userId, SELF_UUID_NAMESPACE); await registerDeviceToken(sessionId, token); } } diff --git a/app/src/services/notifications/notificationService.ts b/app/src/services/notifications/notificationService.ts index 159201e34..a743c7fcb 100644 --- a/app/src/services/notifications/notificationService.ts +++ b/app/src/services/notifications/notificationService.ts @@ -3,7 +3,6 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import { PermissionsAndroid, Platform } from 'react-native'; -import { SELF_UUID_NAMESPACE } from '@env'; import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import messaging from '@react-native-firebase/messaging'; @@ -15,6 +14,8 @@ import { } from '@/services/notifications/notificationService.shared'; import { useSettingStore } from '@/stores/settingStore'; +export const SELF_UUID_NAMESPACE = '00000000-0000-8000-8000-531f00000000'; + export async function getFCMToken(): Promise { try { const token = await messaging().getToken(); @@ -37,10 +38,6 @@ const error = (...args: unknown[]) => { if (!isTestEnv) console.error(...args); }; -export function getSelfUuidNamespace(): string { - return SELF_UUID_NAMESPACE ?? ''; -} - export { getStateMessage }; export async function isNotificationSystemReady(): Promise<{ diff --git a/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx b/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx index 48d37a7d7..14baedc09 100644 --- a/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx +++ b/app/tests/src/screens/kyc/KycSuccessScreen.test.tsx @@ -87,10 +87,10 @@ jest.mock('@/integrations/haptics', () => ({ })); jest.mock('@/services/notifications/notificationService', () => ({ + ...jest.requireActual('@/services/notifications/notificationService'), requestNotificationPermission: jest.fn(), getFCMToken: jest.fn(), registerDeviceToken: jest.fn(), - getSelfUuidNamespace: jest.fn(() => '1eebc0f5-eee9-45a4-9474-a0d103b9f20c'), })); jest.mock('@/config/sentry', () => ({ @@ -119,8 +119,6 @@ const mockUseNavigation = useNavigation as jest.MockedFunction< const { useSelfClient } = jest.requireMock('@selfxyz/mobile-sdk-alpha'); const { useSettingStore } = jest.requireMock('@/stores/settingStore'); -const MOCK_SELF_UUID_NAMESPACE = '1eebc0f5-eee9-45a4-9474-a0d103b9f20c'; - describe('KycSuccessScreen', () => { const mockNavigate = jest.fn(); const mockTrackEvent = jest.fn(); @@ -204,7 +202,7 @@ describe('KycSuccessScreen', () => { await waitFor(() => { // Verify device token was registered with deterministic session ID expect(notificationService.registerDeviceToken).toHaveBeenCalledWith( - uuidv5(mockUserId, MOCK_SELF_UUID_NAMESPACE), + uuidv5(mockUserId, notificationService.SELF_UUID_NAMESPACE), mockFcmToken, ); }); From b3d40d791ac619db592c28bee68340b54e577c9a Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 3 Feb 2026 13:29:16 -0800 Subject: [PATCH 17/19] SELF-1951: prep for sumsub release (#1680) * enable sumsub in mobile sdk * refactor dev settings screen * combine sections * agent feedback * gate kyc button on troubel screens * inline simple sections --- app/src/screens/dev/DevSettingsScreen.tsx | 1017 +---------------- .../dev/components/ErrorInjectionSelector.tsx | 208 ++++ .../dev/components/LogLevelSelector.tsx | 175 +++ .../dev/components/ParameterSection.tsx | 94 ++ .../screens/dev/components/ScreenSelector.tsx | 122 ++ .../dev/components/TopicToggleButton.tsx | 58 + app/src/screens/dev/components/index.ts | 10 + .../screens/dev/hooks/useDangerZoneActions.ts | 197 ++++ .../dev/hooks/useNotificationHandlers.ts | 156 +++ .../dev/sections/DangerZoneSection.tsx | 90 ++ .../dev/sections/DebugShortcutsSection.tsx | 108 ++ .../dev/sections/DevTogglesSection.tsx | 61 + .../dev/sections/PushNotificationsSection.tsx | 54 + app/src/screens/dev/sections/index.ts | 8 + .../scanning/DocumentCameraTroubleScreen.tsx | 34 +- .../scanning/DocumentNFCTroubleScreen.tsx | 20 +- .../documents/selection/IDPickerScreen.tsx | 8 +- app/src/stores/settingStore.ts | 6 + .../mobile-sdk-alpha/src/config/features.ts | 6 +- .../flows/onboarding/id-selection-screen.tsx | 6 +- 20 files changed, 1441 insertions(+), 997 deletions(-) create mode 100644 app/src/screens/dev/components/ErrorInjectionSelector.tsx create mode 100644 app/src/screens/dev/components/LogLevelSelector.tsx create mode 100644 app/src/screens/dev/components/ParameterSection.tsx create mode 100644 app/src/screens/dev/components/ScreenSelector.tsx create mode 100644 app/src/screens/dev/components/TopicToggleButton.tsx create mode 100644 app/src/screens/dev/components/index.ts create mode 100644 app/src/screens/dev/hooks/useDangerZoneActions.ts create mode 100644 app/src/screens/dev/hooks/useNotificationHandlers.ts create mode 100644 app/src/screens/dev/sections/DangerZoneSection.tsx create mode 100644 app/src/screens/dev/sections/DebugShortcutsSection.tsx create mode 100644 app/src/screens/dev/sections/DevTogglesSection.tsx create mode 100644 app/src/screens/dev/sections/PushNotificationsSection.tsx create mode 100644 app/src/screens/dev/sections/index.ts diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index 22f80fff9..a790935c3 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -2,796 +2,53 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import type { PropsWithChildren } from 'react'; -import React, { - cloneElement, - isValidElement, - useEffect, - useMemo, - useState, -} from 'react'; -import type { StyleProp, TextStyle, ViewStyle } from 'react-native'; -import { Alert, Platform, ScrollView, TouchableOpacity } from 'react-native'; -import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import React from 'react'; +import { ScrollView } from 'react-native'; +import { YStack } from 'tamagui'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { Check, ChevronDown, ChevronRight } from '@tamagui/lucide-icons'; -import { - red500, - slate100, - slate200, - slate400, - slate500, - slate600, - slate800, - slate900, - white, - yellow500, -} from '@selfxyz/mobile-sdk-alpha/constants/colors'; -import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks'; import BugIcon from '@/assets/icons/bug_icon.svg'; -import WarningIcon from '@/assets/icons/warning.svg'; import type { RootStackParamList } from '@/navigation'; -import { navigationScreens } from '@/navigation'; -import { unsafe_clearSecrets } from '@/providers/authProvider'; -import { usePassport } from '@/providers/passportDataProvider'; +import { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector'; +import { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { useDangerZoneActions } from '@/screens/dev/hooks/useDangerZoneActions'; +import { useNotificationHandlers } from '@/screens/dev/hooks/useNotificationHandlers'; import { - isNotificationSystemReady, - requestNotificationPermission, - subscribeToTopics, - unsubscribeFromTopics, -} from '@/services/notifications/notificationService'; -import type { InjectedErrorType } from '@/stores/errorInjectionStore'; -import { - ERROR_GROUPS, - ERROR_LABELS, - useErrorInjectionStore, -} from '@/stores/errorInjectionStore'; -import { usePointEventStore } from '@/stores/pointEventStore'; + DangerZoneSection, + DebugShortcutsSection, + DevTogglesSection, + PushNotificationsSection, +} from '@/screens/dev/sections'; import { useSettingStore } from '@/stores/settingStore'; import { IS_DEV_MODE } from '@/utils/devUtils'; -interface TopicToggleButtonProps { - label: string; - isSubscribed: boolean; - onToggle: () => void; -} - -const TopicToggleButton: React.FC = ({ - label, - isSubscribed, - onToggle, -}) => { - return ( - - ); -}; - -interface DevSettingsScreenProps extends PropsWithChildren { - color?: string; - width?: number; - justifyContent?: - | 'center' - | 'unset' - | 'flex-start' - | 'flex-end' - | 'space-between' - | 'space-around' - | 'space-evenly'; - userSelect?: 'all' | 'text' | 'none' | 'contain'; - textAlign?: 'center' | 'left' | 'right'; - style?: StyleProp; -} - -function ParameterSection({ - icon, - title, - description, - darkMode, - children, -}: { - icon: React.ReactNode; - title: string; - description: string; - darkMode?: boolean; - children: React.ReactNode; -}) { - const renderIcon = () => { - const iconElement = - typeof icon === 'function' - ? (icon as () => React.ReactNode)() - : isValidElement(icon) - ? icon - : null; - - return iconElement - ? cloneElement(iconElement as React.ReactElement, { - width: '100%', - height: '100%', - }) - : null; - }; - - return ( - - - - {renderIcon()} - - - - {title} - - - {description} - - - - {children} - - ); -} - -const ScreenSelector = ({}) => { - const navigation = useNavigation(); - const [open, setOpen] = useState(false); - - const screenList = useMemo( - () => - ( - Object.keys(navigationScreens) as (keyof typeof navigationScreens)[] - ).sort(), - [], - ); - - return ( - <> - - - - - - - - - Select screen - - - - - {screenList.map(item => ( - { - setOpen(false); - navigation.navigate(item as never); - }} - > - - - {item} - - - - ))} - - - - - - ); -}; - -const LogLevelSelector = ({ - currentLevel, - onSelect, -}: { - currentLevel: string; - onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void; -}) => { - const [open, setOpen] = useState(false); - - const logLevels = ['debug', 'info', 'warn', 'error'] as const; - - return ( - <> - - - - - - - - - Select log level - - - - - {logLevels.map(level => ( - { - setOpen(false); - onSelect(level); - }} - > - - - {level.toUpperCase()} - - {currentLevel === level && ( - - )} - - - ))} - - - - - - ); -}; - -const ErrorInjectionSelector = () => { - const injectedErrors = useErrorInjectionStore(state => state.injectedErrors); - const setInjectedErrors = useErrorInjectionStore( - state => state.setInjectedErrors, - ); - const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors); - const [open, setOpen] = useState(false); - - // Single error selection - replace instead of toggle - const selectError = (errorType: InjectedErrorType) => { - // If clicking the same error, clear it; otherwise set the new one - if (injectedErrors.length === 1 && injectedErrors[0] === errorType) { - clearAllErrors(); - } else { - setInjectedErrors([errorType]); - } - // Close the sheet after selection - setOpen(false); - }; - - const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null; - const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null; - - return ( - - - - {currentError && ( - - )} - - - - - - - - Onboarding Error Testing - - - - - {Object.entries(ERROR_GROUPS).map(([groupName, errors]) => ( - - - {groupName} - - {errors.map((errorType: InjectedErrorType) => ( - selectError(errorType)} - > - - - {ERROR_LABELS[errorType]} - - {currentError === errorType && ( - - )} - - - ))} - - ))} - - - - - - ); -}; - -const DevSettingsScreen: React.FC = ({}) => { - const { clearDocumentCatalogForMigrationTesting } = usePassport(); - const clearPointEvents = usePointEventStore(state => state.clearEvents); - const { resetBackupForPoints } = useSettingStore(); +const DevSettingsScreen: React.FC = () => { const navigation = useNavigation() as NativeStackScreenProps['navigation']; - const subscribedTopics = useSettingStore(state => state.subscribedTopics); + const paddingBottom = useSafeBottomPadding(20); + + // Settings store const loggingSeverity = useSettingStore(state => state.loggingSeverity); const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity); const useStrongBox = useSettingStore(state => state.useStrongBox); const setUseStrongBox = useSettingStore(state => state.setUseStrongBox); - const [hasNotificationPermission, setHasNotificationPermission] = - useState(false); - const paddingBottom = useSafeBottomPadding(20); + const kycEnabled = useSettingStore(state => state.kycEnabled); + const setKycEnabled = useSettingStore(state => state.setKycEnabled); - // Check notification permissions on mount - useEffect(() => { - const checkPermissions = async () => { - const readiness = await isNotificationSystemReady(); - setHasNotificationPermission(readiness.ready); - }; - checkPermissions(); - }, []); - - const handleTopicToggle = async (topics: string[], topicLabel: string) => { - // Check permissions first - if (!hasNotificationPermission) { - Alert.alert( - 'Permissions Required', - 'Push notifications are not enabled. Would you like to enable them?', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Enable', - onPress: async () => { - try { - const granted = await requestNotificationPermission(); - if (granted) { - // Update permission state - setHasNotificationPermission(true); - Alert.alert( - 'Success', - 'Permissions granted! You can now subscribe to topics.', - [{ text: 'OK' }], - ); - } else { - Alert.alert( - 'Failed', - 'Could not enable notifications. Please enable them in your device Settings.', - [{ text: 'OK' }], - ); - } - } catch (error) { - Alert.alert( - 'Error', - error instanceof Error - ? error.message - : 'Failed to request permissions', - [{ text: 'OK' }], - ); - } - }, - }, - ], - ); - return; - } - - const isCurrentlySubscribed = topics.every(topic => - subscribedTopics.includes(topic), - ); - - if (isCurrentlySubscribed) { - // Show confirmation dialog for unsubscribe - Alert.alert( - 'Disable Notifications', - `Are you sure you want to disable push notifications for ${topicLabel}?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Disable', - style: 'destructive', - onPress: async () => { - try { - const result = await unsubscribeFromTopics(topics); - if (result.successes.length > 0) { - Alert.alert( - 'Success', - `Disabled notifications for ${topicLabel}`, - [{ text: 'OK' }], - ); - } else { - Alert.alert( - 'Error', - `Failed to disable: ${result.failures.map(f => f.error).join(', ')}`, - [{ text: 'OK' }], - ); - } - } catch (error) { - Alert.alert( - 'Error', - error instanceof Error - ? error.message - : 'Failed to unsubscribe', - [{ text: 'OK' }], - ); - } - }, - }, - ], - ); - } else { - // Subscribe without confirmation - try { - const result = await subscribeToTopics(topics); - if (result.successes.length > 0) { - Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [ - { text: 'OK' }, - ]); - } else { - Alert.alert( - 'Error', - `Failed to enable: ${result.failures.map(f => f.error).join(', ')}`, - [{ text: 'OK' }], - ); - } - } catch (error) { - Alert.alert( - 'Error', - error instanceof Error ? error.message : 'Failed to subscribe', - [{ text: 'OK' }], - ); - } - } - }; - - const handleClearSecretsPress = () => { - Alert.alert( - 'Delete Keychain Secrets', - "Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.", - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - await unsafe_clearSecrets(); - }, - }, - ], - ); - }; - - const handleClearDocumentCatalogPress = () => { - Alert.alert( - 'Clear Document Catalog', - 'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - await clearDocumentCatalogForMigrationTesting(); - }, - }, - ], - ); - }; - - const handleClearPointEventsPress = () => { - Alert.alert( - 'Clear Point Events', - 'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - await clearPointEvents(); - Alert.alert('Success', 'Point events cleared successfully.', [ - { text: 'OK' }, - ]); - }, - }, - ], - ); - }; - - const handleResetBackupStatePress = () => { - Alert.alert( - 'Reset Backup State', - 'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Reset', - style: 'destructive', - onPress: () => { - resetBackupForPoints(); - Alert.alert('Success', 'Backup state reset successfully.', [ - { text: 'OK' }, - ]); - }, - }, - ], - ); - }; - - const handleClearBackupEventsPress = () => { - Alert.alert( - 'Clear Backup Events', - 'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Clear', - style: 'destructive', - onPress: async () => { - const events = usePointEventStore.getState().events; - const backupEvents = events.filter( - event => event.type === 'backup', - ); - for (const event of backupEvents) { - await usePointEventStore.getState().removeEvent(event.id); - } - Alert.alert('Success', 'Backup events cleared successfully.', [ - { text: 'OK' }, - ]); - }, - }, - ], - ); - }; + // Custom hooks + const { hasNotificationPermission, subscribedTopics, handleTopicToggle } = + useNotificationHandlers(); + const { + handleClearSecretsPress, + handleClearDocumentCatalogPress, + handleClearPointEventsPress, + handleResetBackupStatePress, + handleClearBackupEventsPress, + } = useDangerZoneActions(); return ( @@ -804,121 +61,22 @@ const DevSettingsScreen: React.FC = ({}) => { paddingTop="$4" paddingBottom={paddingBottom} > - } - title="Debug Shortcuts" - description="Jump directly to any screen for testing" - > - - - - {IS_DEV_MODE && ( - - )} - - - + - } - title="Push Notifications" - description="Manage topic subscriptions" - > - - handleTopicToggle(['nova'], 'Starfall')} - /> - handleTopicToggle(['general'], 'General')} - /> - - handleTopicToggle(['nova', 'general'], 'both topics') - } - /> - - + {IS_DEV_MODE && ( + + )} + + } @@ -941,86 +99,13 @@ const DevSettingsScreen: React.FC = ({}) => { )} - {Platform.OS === 'android' && ( - } - title="Android Keystore" - description="Configure keystore security options" - > - { - Alert.alert( - useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox', - useStrongBox - ? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.' - : 'New keys will attempt to use StrongBox hardware backing for enhanced security.', - [ - { text: 'Cancel', style: 'cancel' }, - { - text: useStrongBox ? 'Disable' : 'Enable', - onPress: () => setUseStrongBox(!useStrongBox), - }, - ], - ); - }} - /> - - )} - - } - title="Danger Zone" - description="These actions are sensitive" - darkMode={true} - > - {[ - { - label: 'Delete your private key', - onPress: handleClearSecretsPress, - dangerTheme: true, - }, - { - label: 'Clear document catalog', - onPress: handleClearDocumentCatalogPress, - dangerTheme: true, - }, - { - label: 'Clear point events', - onPress: handleClearPointEventsPress, - dangerTheme: true, - }, - { - label: 'Reset backup state', - onPress: handleResetBackupStatePress, - dangerTheme: true, - }, - { - label: 'Clear backup events', - onPress: handleClearBackupEventsPress, - dangerTheme: true, - }, - ].map(({ label, onPress, dangerTheme }) => ( - - ))} - + ); diff --git a/app/src/screens/dev/components/ErrorInjectionSelector.tsx b/app/src/screens/dev/components/ErrorInjectionSelector.tsx new file mode 100644 index 000000000..7feb243f0 --- /dev/null +++ b/app/src/screens/dev/components/ErrorInjectionSelector.tsx @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useRef, useState } from 'react'; +import { ScrollView, TouchableOpacity } from 'react-native'; +import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import { Check, ChevronDown } from '@tamagui/lucide-icons'; + +import { + red500, + slate200, + slate500, + slate600, + slate800, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import type { InjectedErrorType } from '@/stores/errorInjectionStore'; +import { + ERROR_GROUPS, + ERROR_LABELS, + useErrorInjectionStore, +} from '@/stores/errorInjectionStore'; +import { + registerModalCallbacks, + unregisterModalCallbacks, +} from '@/utils/modalCallbackRegistry'; + +export const ErrorInjectionSelector = () => { + const injectedErrors = useErrorInjectionStore(state => state.injectedErrors); + const setInjectedErrors = useErrorInjectionStore( + state => state.setInjectedErrors, + ); + const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors); + const [open, setOpen] = useState(false); + const callbackIdRef = useRef(); + + const handleModalDismiss = useCallback(() => { + setOpen(false); + if (callbackIdRef.current !== undefined) { + unregisterModalCallbacks(callbackIdRef.current); + callbackIdRef.current = undefined; + } + }, []); + + const openSheet = useCallback(() => { + setOpen(true); + const id = registerModalCallbacks({ + onButtonPress: () => {}, + onModalDismiss: handleModalDismiss, + }); + callbackIdRef.current = id; + }, [handleModalDismiss]); + + const closeSheet = useCallback(() => { + handleModalDismiss(); + }, [handleModalDismiss]); + + const handleSheetOpenChange = useCallback( + (isOpen: boolean) => { + if (!isOpen) { + handleModalDismiss(); + } else { + setOpen(isOpen); + } + }, + [handleModalDismiss], + ); + + // Single error selection - replace instead of toggle + const selectError = (errorType: InjectedErrorType) => { + // If clicking the same error, clear it; otherwise set the new one + if (injectedErrors.length === 1 && injectedErrors[0] === errorType) { + clearAllErrors(); + } else { + setInjectedErrors([errorType]); + } + // Close the sheet after selection + closeSheet(); + }; + + const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null; + const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null; + + return ( + + + + {currentError && ( + + )} + + + + + + + + Onboarding Error Testing + + + + + {Object.entries(ERROR_GROUPS).map(([groupName, errors]) => ( + + + {groupName} + + {errors.map((errorType: InjectedErrorType) => ( + selectError(errorType)} + > + + + {ERROR_LABELS[errorType]} + + {currentError === errorType && ( + + )} + + + ))} + + ))} + + + + + + ); +}; diff --git a/app/src/screens/dev/components/LogLevelSelector.tsx b/app/src/screens/dev/components/LogLevelSelector.tsx new file mode 100644 index 000000000..d919669dd --- /dev/null +++ b/app/src/screens/dev/components/LogLevelSelector.tsx @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { ScrollView, TouchableOpacity } from 'react-native'; +import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import { Check, ChevronDown } from '@tamagui/lucide-icons'; + +import { + slate200, + slate500, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import { + registerModalCallbacks, + unregisterModalCallbacks, +} from '@/utils/modalCallbackRegistry'; + +interface LogLevelSelectorProps { + currentLevel: string; + onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void; +} + +export const LogLevelSelector: React.FC = ({ + currentLevel, + onSelect, +}) => { + const [open, setOpen] = useState(false); + const callbackIdRef = useRef(); + + const logLevels = ['debug', 'info', 'warn', 'error'] as const; + + // Cleanup effect to unregister callbacks on unmount + useEffect(() => { + return () => { + if (callbackIdRef.current !== undefined) { + unregisterModalCallbacks(callbackIdRef.current); + callbackIdRef.current = undefined; + } + }; + }, []); + + const handleModalDismiss = useCallback(() => { + setOpen(false); + if (callbackIdRef.current !== undefined) { + unregisterModalCallbacks(callbackIdRef.current); + callbackIdRef.current = undefined; + } + }, []); + + const openSheet = useCallback(() => { + setOpen(true); + const id = registerModalCallbacks({ + onButtonPress: () => {}, + onModalDismiss: handleModalDismiss, + }); + callbackIdRef.current = id; + }, [handleModalDismiss]); + + const closeSheet = useCallback(() => { + handleModalDismiss(); + }, [handleModalDismiss]); + + const handleSheetOpenChange = useCallback( + (isOpen: boolean) => { + if (!isOpen) { + handleModalDismiss(); + } else { + setOpen(isOpen); + } + }, + [handleModalDismiss], + ); + + const handleLevelSelect = useCallback( + (level: 'debug' | 'info' | 'warn' | 'error') => { + closeSheet(); + onSelect(level); + }, + [closeSheet, onSelect], + ); + + return ( + <> + + + + + + + + + Select log level + + + + + {logLevels.map(level => ( + handleLevelSelect(level)} + > + + + {level.toUpperCase()} + + {currentLevel === level && ( + + )} + + + ))} + + + + + + ); +}; diff --git a/app/src/screens/dev/components/ParameterSection.tsx b/app/src/screens/dev/components/ParameterSection.tsx new file mode 100644 index 000000000..f99dc6c25 --- /dev/null +++ b/app/src/screens/dev/components/ParameterSection.tsx @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { PropsWithChildren } from 'react'; +import React, { cloneElement, isValidElement } from 'react'; +import { Text, XStack, YStack } from 'tamagui'; + +import { + slate100, + slate200, + slate400, + slate600, + slate800, + slate900, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +interface ParameterSectionProps extends PropsWithChildren { + icon: React.ReactNode; + title: string; + description: string; + darkMode?: boolean; +} + +export function ParameterSection({ + icon, + title, + description, + darkMode, + children, +}: ParameterSectionProps) { + const renderIcon = () => { + const iconElement = + typeof icon === 'function' + ? (icon as () => React.ReactNode)() + : isValidElement(icon) + ? icon + : null; + + return iconElement + ? cloneElement(iconElement as React.ReactElement, { + width: '100%', + height: '100%', + }) + : null; + }; + + return ( + + + + {renderIcon()} + + + + {title} + + + {description} + + + + {children} + + ); +} diff --git a/app/src/screens/dev/components/ScreenSelector.tsx b/app/src/screens/dev/components/ScreenSelector.tsx new file mode 100644 index 000000000..0e3084684 --- /dev/null +++ b/app/src/screens/dev/components/ScreenSelector.tsx @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useMemo, useState } from 'react'; +import { ScrollView, TouchableOpacity } from 'react-native'; +import { Button, Sheet, Text, XStack, YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import { ChevronDown } from '@tamagui/lucide-icons'; + +import { + slate200, + slate500, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import { navigationScreens } from '@/navigation'; + +export const ScreenSelector = () => { + const navigation = useNavigation(); + const [open, setOpen] = useState(false); + + const screenList = useMemo( + () => + ( + Object.keys(navigationScreens) as (keyof typeof navigationScreens)[] + ).sort(), + [], + ); + + return ( + <> + + + + + + + + + Select screen + + + + + {screenList.map(item => ( + { + setOpen(false); + navigation.navigate(item as never); + }} + > + + + {item} + + + + ))} + + + + + + ); +}; diff --git a/app/src/screens/dev/components/TopicToggleButton.tsx b/app/src/screens/dev/components/TopicToggleButton.tsx new file mode 100644 index 000000000..5aaa89503 --- /dev/null +++ b/app/src/screens/dev/components/TopicToggleButton.tsx @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Button, Text } from 'tamagui'; + +import { + slate200, + slate400, + slate600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +export interface TopicToggleButtonProps { + label: string; + isSubscribed: boolean; + onToggle: () => void; +} + +export const TopicToggleButton: React.FC = ({ + label, + isSubscribed, + onToggle, +}) => { + return ( + + ); +}; diff --git a/app/src/screens/dev/components/index.ts b/app/src/screens/dev/components/index.ts new file mode 100644 index 000000000..4193b34e0 --- /dev/null +++ b/app/src/screens/dev/components/index.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +export type { TopicToggleButtonProps } from '@/screens/dev/components/TopicToggleButton'; +export { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector'; +export { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector'; +export { ParameterSection } from '@/screens/dev/components/ParameterSection'; +export { ScreenSelector } from '@/screens/dev/components/ScreenSelector'; +export { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton'; diff --git a/app/src/screens/dev/hooks/useDangerZoneActions.ts b/app/src/screens/dev/hooks/useDangerZoneActions.ts new file mode 100644 index 000000000..9b90b8860 --- /dev/null +++ b/app/src/screens/dev/hooks/useDangerZoneActions.ts @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { Alert } from 'react-native'; + +import { unsafe_clearSecrets } from '@/providers/authProvider'; +import { usePassport } from '@/providers/passportDataProvider'; +import { usePointEventStore } from '@/stores/pointEventStore'; +import { useSettingStore } from '@/stores/settingStore'; + +export const useDangerZoneActions = () => { + const { clearDocumentCatalogForMigrationTesting } = usePassport(); + const clearPointEvents = usePointEventStore(state => state.clearEvents); + const { resetBackupForPoints } = useSettingStore(); + + const handleClearSecretsPress = () => { + Alert.alert( + 'Delete Keychain Secrets', + "Are you sure you want to remove your keychain secrets?\n\nIf this secret is not backed up, your account will be lost and the ID documents attached to it won't be usable.", + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + try { + await unsafe_clearSecrets(); + Alert.alert('Success', 'Keychain secrets cleared successfully.', [ + { text: 'OK' }, + ]); + } catch (error) { + console.error( + 'Failed to clear keychain secrets:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear keychain secrets. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + const handleClearDocumentCatalogPress = () => { + Alert.alert( + 'Clear Document Catalog', + 'Are you sure you want to clear the document catalog?\n\nThis will remove all documents from the new storage system but preserve legacy storage for migration testing. You will need to restart the app to test migration.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + try { + await clearDocumentCatalogForMigrationTesting(); + Alert.alert( + 'Success', + 'Document catalog cleared successfully. Please restart the app to test migration.', + [{ text: 'OK' }], + ); + } catch (error) { + console.error( + 'Failed to clear document catalog:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear document catalog. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + const handleClearPointEventsPress = () => { + Alert.alert( + 'Clear Point Events', + 'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + try { + await clearPointEvents(); + Alert.alert('Success', 'Point events cleared successfully.', [ + { text: 'OK' }, + ]); + } catch (error) { + console.error( + 'Failed to clear point events:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear point events. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + const handleResetBackupStatePress = () => { + Alert.alert( + 'Reset Backup State', + 'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Reset', + style: 'destructive', + onPress: () => { + resetBackupForPoints(); + Alert.alert('Success', 'Backup state reset successfully.', [ + { text: 'OK' }, + ]); + }, + }, + ], + ); + }; + + const handleClearBackupEventsPress = () => { + Alert.alert( + 'Clear Backup Events', + 'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + try { + const events = usePointEventStore.getState().events; + const backupEvents = events.filter( + event => event.type === 'backup', + ); + await Promise.all( + backupEvents.map(event => + usePointEventStore.getState().removeEvent(event.id), + ), + ); + Alert.alert('Success', 'Backup events cleared successfully.', [ + { text: 'OK' }, + ]); + } catch (error) { + console.error( + 'Failed to clear backup events:', + error instanceof Error ? error.message : String(error), + ); + Alert.alert( + 'Error', + 'Failed to clear backup events. Please try again.', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + }; + + return { + handleClearSecretsPress, + handleClearDocumentCatalogPress, + handleClearPointEventsPress, + handleResetBackupStatePress, + handleClearBackupEventsPress, + }; +}; diff --git a/app/src/screens/dev/hooks/useNotificationHandlers.ts b/app/src/screens/dev/hooks/useNotificationHandlers.ts new file mode 100644 index 000000000..de23c95cd --- /dev/null +++ b/app/src/screens/dev/hooks/useNotificationHandlers.ts @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback, useEffect, useState } from 'react'; +import { Alert, AppState } from 'react-native'; + +import { + isNotificationSystemReady, + requestNotificationPermission, + subscribeToTopics, + unsubscribeFromTopics, +} from '@/services/notifications/notificationService'; +import { useSettingStore } from '@/stores/settingStore'; + +export const useNotificationHandlers = () => { + const subscribedTopics = useSettingStore(state => state.subscribedTopics); + const [hasNotificationPermission, setHasNotificationPermission] = + useState(false); + + const checkPermissions = useCallback(async () => { + const readiness = await isNotificationSystemReady(); + setHasNotificationPermission(readiness.ready); + }, []); + + // Check notification permissions on mount and when app regains focus + useEffect(() => { + checkPermissions(); + + const subscription = AppState.addEventListener('change', nextAppState => { + if (nextAppState === 'active') { + checkPermissions(); + } + }); + + return () => subscription.remove(); + }, [checkPermissions]); + + const handleTopicToggle = async (topics: string[], topicLabel: string) => { + // Check permissions first + if (!hasNotificationPermission) { + Alert.alert( + 'Permissions Required', + 'Push notifications are not enabled. Would you like to enable them?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Enable', + onPress: async () => { + try { + const granted = await requestNotificationPermission(); + if (granted) { + // Update permission state + setHasNotificationPermission(true); + Alert.alert( + 'Success', + 'Permissions granted! You can now subscribe to topics.', + [{ text: 'OK' }], + ); + } else { + Alert.alert( + 'Failed', + 'Could not enable notifications. Please enable them in your device Settings.', + [{ text: 'OK' }], + ); + } + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error + ? error.message + : 'Failed to request permissions', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + return; + } + + const isCurrentlySubscribed = topics.every(topic => + subscribedTopics.includes(topic), + ); + + if (isCurrentlySubscribed) { + // Show confirmation dialog for unsubscribe + Alert.alert( + 'Disable Notifications', + `Are you sure you want to disable push notifications for ${topicLabel}?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Disable', + style: 'destructive', + onPress: async () => { + try { + const result = await unsubscribeFromTopics(topics); + if (result.successes.length > 0) { + Alert.alert( + 'Success', + `Disabled notifications for ${topicLabel}`, + [{ text: 'OK' }], + ); + } else { + Alert.alert( + 'Error', + `Failed to disable: ${result.failures.map(f => f.error).join(', ')}`, + [{ text: 'OK' }], + ); + } + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error + ? error.message + : 'Failed to unsubscribe', + [{ text: 'OK' }], + ); + } + }, + }, + ], + ); + } else { + // Subscribe without confirmation + try { + const result = await subscribeToTopics(topics); + if (result.successes.length > 0) { + Alert.alert('✅ Success', `Enabled notifications for ${topicLabel}`, [ + { text: 'OK' }, + ]); + } else { + Alert.alert( + 'Error', + `Failed to enable: ${result.failures.map(f => f.error).join(', ')}`, + [{ text: 'OK' }], + ); + } + } catch (error) { + Alert.alert( + 'Error', + error instanceof Error ? error.message : 'Failed to subscribe', + [{ text: 'OK' }], + ); + } + } + }; + + return { + hasNotificationPermission, + subscribedTopics, + handleTopicToggle, + }; +}; diff --git a/app/src/screens/dev/sections/DangerZoneSection.tsx b/app/src/screens/dev/sections/DangerZoneSection.tsx new file mode 100644 index 000000000..10f6a8565 --- /dev/null +++ b/app/src/screens/dev/sections/DangerZoneSection.tsx @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Button, Text } from 'tamagui'; + +import { + red500, + slate500, + white, + yellow500, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import WarningIcon from '@/assets/icons/warning.svg'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; + +interface DangerZoneSectionProps { + onClearSecrets: () => void; + onClearDocumentCatalog: () => void; + onClearPointEvents: () => void; + onResetBackupState: () => void; + onClearBackupEvents: () => void; +} + +export const DangerZoneSection: React.FC = ({ + onClearSecrets, + onClearDocumentCatalog, + onClearPointEvents, + onResetBackupState, + onClearBackupEvents, +}) => { + const dangerActions = [ + { + label: 'Delete your private key', + onPress: onClearSecrets, + dangerTheme: true, + }, + { + label: 'Clear document catalog', + onPress: onClearDocumentCatalog, + dangerTheme: true, + }, + { + label: 'Clear point events', + onPress: onClearPointEvents, + dangerTheme: true, + }, + { + label: 'Reset backup state', + onPress: onResetBackupState, + dangerTheme: true, + }, + { + label: 'Clear backup events', + onPress: onClearBackupEvents, + dangerTheme: true, + }, + ]; + + return ( + } + title="Danger Zone" + description="These actions are sensitive" + darkMode={true} + > + {dangerActions.map(({ label, onPress, dangerTheme }) => ( + + ))} + + ); +}; diff --git a/app/src/screens/dev/sections/DebugShortcutsSection.tsx b/app/src/screens/dev/sections/DebugShortcutsSection.tsx new file mode 100644 index 000000000..d44903ad6 --- /dev/null +++ b/app/src/screens/dev/sections/DebugShortcutsSection.tsx @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Button, Text, XStack, YStack } from 'tamagui'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { ChevronRight } from '@tamagui/lucide-icons'; + +import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import BugIcon from '@/assets/icons/bug_icon.svg'; +import type { RootStackParamList } from '@/navigation'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { ScreenSelector } from '@/screens/dev/components/ScreenSelector'; +import { IS_DEV_MODE } from '@/utils/devUtils'; + +interface DebugShortcutsSectionProps { + navigation: NativeStackNavigationProp; +} + +export const DebugShortcutsSection: React.FC = ({ + navigation, +}) => { + return ( + } + title="Debug Shortcuts" + description="Jump directly to any screen for testing" + > + + + + {IS_DEV_MODE && ( + + )} + + + + ); +}; diff --git a/app/src/screens/dev/sections/DevTogglesSection.tsx b/app/src/screens/dev/sections/DevTogglesSection.tsx new file mode 100644 index 000000000..0c2972b6c --- /dev/null +++ b/app/src/screens/dev/sections/DevTogglesSection.tsx @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Alert, Platform } from 'react-native'; + +import BugIcon from '@/assets/icons/bug_icon.svg'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton'; + +interface DevTogglesSectionProps { + kycEnabled: boolean; + setKycEnabled: (enabled: boolean) => void; + useStrongBox: boolean; + setUseStrongBox: (useStrongBox: boolean) => void; +} + +export const DevTogglesSection: React.FC = ({ + kycEnabled, + setKycEnabled, + useStrongBox, + setUseStrongBox, +}) => { + const handleToggleStrongBox = () => { + Alert.alert( + useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox', + useStrongBox + ? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.' + : 'New keys will attempt to use StrongBox hardware backing for enhanced security.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: useStrongBox ? 'Disable' : 'Enable', + onPress: () => setUseStrongBox(!useStrongBox), + }, + ], + ); + }; + + return ( + } + title="Options" + description="Development and security options" + > + setKycEnabled(!kycEnabled)} + /> + {Platform.OS === 'android' && ( + + )} + + ); +}; diff --git a/app/src/screens/dev/sections/PushNotificationsSection.tsx b/app/src/screens/dev/sections/PushNotificationsSection.tsx new file mode 100644 index 000000000..98771d826 --- /dev/null +++ b/app/src/screens/dev/sections/PushNotificationsSection.tsx @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { YStack } from 'tamagui'; + +import BugIcon from '@/assets/icons/bug_icon.svg'; +import { ParameterSection } from '@/screens/dev/components/ParameterSection'; +import { TopicToggleButton } from '@/screens/dev/components/TopicToggleButton'; + +interface PushNotificationsSectionProps { + hasNotificationPermission: boolean; + subscribedTopics: string[]; + onTopicToggle: (topics: string[], topicLabel: string) => void; +} + +export const PushNotificationsSection: React.FC< + PushNotificationsSectionProps +> = ({ hasNotificationPermission, subscribedTopics, onTopicToggle }) => { + return ( + } + title="Push Notifications" + description="Manage topic subscriptions" + > + + onTopicToggle(['nova'], 'Starfall')} + /> + onTopicToggle(['general'], 'General')} + /> + onTopicToggle(['nova', 'general'], 'both topics')} + /> + + + ); +}; diff --git a/app/src/screens/dev/sections/index.ts b/app/src/screens/dev/sections/index.ts new file mode 100644 index 000000000..2981b8244 --- /dev/null +++ b/app/src/screens/dev/sections/index.ts @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +export { DangerZoneSection } from '@/screens/dev/sections/DangerZoneSection'; +export { DebugShortcutsSection } from '@/screens/dev/sections/DebugShortcutsSection'; +export { DevTogglesSection } from '@/screens/dev/sections/DevTogglesSection'; +export { PushNotificationsSection } from '@/screens/dev/sections/PushNotificationsSection'; diff --git a/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx b/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx index c99d5e242..684d7b8f5 100644 --- a/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentCameraTroubleScreen.tsx @@ -20,6 +20,7 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import { useSumsubLauncher } from '@/hooks/useSumsubLauncher'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; import { flush as flushAnalytics } from '@/services/analytics'; +import { useSettingStore } from '@/stores/settingStore'; const tips: TipProps[] = [ { @@ -54,6 +55,7 @@ const DocumentCameraTroubleScreen: React.FC = () => { const selfClient = useSelfClient(); const { useMRZStore } = selfClient; const { countryCode } = useMRZStore(); + const kycEnabled = useSettingStore(state => state.kycEnabled); const { launchSumsubVerification, isLoading } = useSumsubLauncher({ countryCode, errorSource: 'sumsub_initialization', @@ -80,21 +82,25 @@ const DocumentCameraTroubleScreen: React.FC = () => { page quickly and clearly! - - Or try an alternative verification method: - + {kycEnabled && ( + <> + + Or try an alternative verification method: + - - {isLoading ? 'Loading...' : 'Try Alternative Verification'} - + + {isLoading ? 'Loading...' : 'Try Alternative Verification'} + + + )} } > diff --git a/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx b/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx index 0af2728d0..a75e96c36 100644 --- a/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentNFCTroubleScreen.tsx @@ -19,6 +19,7 @@ import { useSumsubLauncher } from '@/hooks/useSumsubLauncher'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; import { flushAllAnalytics } from '@/services/analytics'; import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support'; +import { useSettingStore } from '@/stores/settingStore'; const tips: TipProps[] = [ { @@ -55,6 +56,7 @@ const DocumentNFCTroubleScreen: React.FC = () => { const selfClient = useSelfClient(); const { useMRZStore } = selfClient; const { countryCode } = useMRZStore(); + const kycEnabled = useSettingStore(state => state.kycEnabled); const { launchSumsubVerification, isLoading } = useSumsubLauncher({ countryCode, errorSource: 'sumsub_initialization', @@ -89,14 +91,16 @@ const DocumentNFCTroubleScreen: React.FC = () => { {SUPPORT_FORM_BUTTON_TEXT} - - {isLoading ? 'Loading...' : 'Try Alternative Verification'} - + {kycEnabled && ( + + {isLoading ? 'Loading...' : 'Try Alternative Verification'} + + )} } > diff --git a/app/src/screens/documents/selection/IDPickerScreen.tsx b/app/src/screens/documents/selection/IDPickerScreen.tsx index 3d8415110..f3164f34d 100644 --- a/app/src/screens/documents/selection/IDPickerScreen.tsx +++ b/app/src/screens/documents/selection/IDPickerScreen.tsx @@ -13,6 +13,7 @@ import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-scree import { DocumentFlowNavBar } from '@/components/navbar/DocumentFlowNavBar'; import type { RootStackParamList } from '@/navigation'; +import { useSettingStore } from '@/stores/settingStore'; import { extraYPadding } from '@/utils/styleUtils'; type IDPickerScreenRouteProp = RouteProp; @@ -21,6 +22,7 @@ const IDPickerScreen: React.FC = () => { const route = useRoute(); const { countryCode = '', documentTypes = [] } = route.params || {}; const bottom = useSafeAreaInsets().bottom; + const kycEnabled = useSettingStore(state => state.kycEnabled); return ( { paddingBottom={bottom + extraYPadding + 24} > - + ); }; diff --git a/app/src/stores/settingStore.ts b/app/src/stores/settingStore.ts index eee144d61..abecfc444 100644 --- a/app/src/stores/settingStore.ts +++ b/app/src/stores/settingStore.ts @@ -21,6 +21,7 @@ interface PersistedSettingsState { homeScreenViewCount: number; incrementHomeScreenViewCount: () => void; isDevMode: boolean; + kycEnabled: boolean; loggingSeverity: LoggingSeverity; pointsAddress: string | null; removeSubscribedTopic: (topic: string) => void; @@ -32,6 +33,7 @@ interface PersistedSettingsState { setFcmToken: (token: string | null) => void; setHasViewedRecoveryPhrase: (viewed: boolean) => void; setKeychainMigrationCompleted: () => void; + setKycEnabled: (enabled: boolean) => void; setLoggingSeverity: (severity: LoggingSeverity) => void; setPointsAddress: (address: string | null) => void; setSkipDocumentSelector: (value: boolean) => void; @@ -148,6 +150,10 @@ export const useSettingStore = create()( useStrongBox: false, setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }), + // KYC flow toggle (default: false, dev-only feature) + kycEnabled: false, + setKycEnabled: (enabled: boolean) => set({ kycEnabled: enabled }), + // Non-persisted state (will not be saved to storage) hideNetworkModal: false, setHideNetworkModal: (hideNetworkModal: boolean) => { diff --git a/packages/mobile-sdk-alpha/src/config/features.ts b/packages/mobile-sdk-alpha/src/config/features.ts index 488e4a5b4..596851505 100644 --- a/packages/mobile-sdk-alpha/src/config/features.ts +++ b/packages/mobile-sdk-alpha/src/config/features.ts @@ -8,9 +8,5 @@ * Set to true when ready to launch the feature. */ export const FeatureFlags = { - /** - * Enable Sumsub/KYC "Other IDs" option in the ID selection screen. - * When false, the KYC button will be hidden from users. - */ - KYC_ENABLED: false, + // Add new flags here as needed } as const; diff --git a/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx b/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx index 3f3bfd973..4803fff46 100644 --- a/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx +++ b/packages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsx @@ -11,7 +11,6 @@ import PassportCameraScanIcon from '../../../svgs/icons/passport_camera_scan.svg import PlusIcon from '../../../svgs/icons/plus.svg'; import SelfLogo from '../../../svgs/logo.svg'; import { BodyText, RoundFlag, View, XStack, YStack } from '../../components'; -import { FeatureFlags } from '../../config/features'; import { black, blue100, blue600, slate100, slate300, slate400, white } from '../../constants/colors'; import { advercase, dinot } from '../../constants/fonts'; import { useSelfClient } from '../../context'; @@ -129,10 +128,11 @@ const DocumentItem: React.FC = ({ docType, onPress }) => { type IDSelectionScreenProps = { countryCode: string; documentTypes: string[]; + showKyc?: boolean; }; const IDSelectionScreen: React.FC = props => { - const { countryCode = '', documentTypes = [] } = props; + const { countryCode = '', documentTypes = [], showKyc = false } = props; const selfClient = useSelfClient(); const onSelectDocumentType = (docType: string) => { @@ -173,7 +173,7 @@ const IDSelectionScreen: React.FC = props => { onSelectDocumentType(docType)} /> ))} Be sure your document is ready to scan - {FeatureFlags.KYC_ENABLED && ( + {showKyc && ( onSelectDocumentType('kyc')} /> From 0debcac6483858a25654ee56eb34f80c5928926a Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 3 Feb 2026 14:09:23 -0800 Subject: [PATCH 18/19] wrap with error boundary (#1688) --- app/src/screens/dev/DevSettingsScreen.tsx | 103 +++++++++++----------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index a790935c3..bda2d495a 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -11,6 +11,7 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks'; import BugIcon from '@/assets/icons/bug_icon.svg'; +import ErrorBoundary from '@/components/ErrorBoundary'; import type { RootStackParamList } from '@/navigation'; import { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector'; import { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector'; @@ -51,63 +52,65 @@ const DevSettingsScreen: React.FC = () => { } = useDangerZoneActions(); return ( - - - - - {IS_DEV_MODE && ( - - )} - - - - } - title="Log Level" - description="Configure logging verbosity" + + + - - + + + {IS_DEV_MODE && ( + + )} + + - {IS_DEV_MODE && ( } - title="Onboarding Error Testing" - description="Test onboarding error flows" + title="Log Level" + description="Configure logging verbosity" > - + - )} - - - + {IS_DEV_MODE && ( + } + title="Onboarding Error Testing" + description="Test onboarding error flows" + > + + + )} + + + + + ); }; From aba6d97dbeb6e058b2c5d651b2f92acec61fcfe9 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 3 Feb 2026 19:36:49 -0800 Subject: [PATCH 19/19] fix patch for ci (#1689) --- ...2.30.0.patch => react-native-gesture-handler+2.19.0.patch} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename patches/{react-native-gesture-handler+2.30.0.patch => react-native-gesture-handler+2.19.0.patch} (96%) diff --git a/patches/react-native-gesture-handler+2.30.0.patch b/patches/react-native-gesture-handler+2.19.0.patch similarity index 96% rename from patches/react-native-gesture-handler+2.30.0.patch rename to patches/react-native-gesture-handler+2.19.0.patch index 05cb86259..eba9af8aa 100644 --- a/patches/react-native-gesture-handler+2.30.0.patch +++ b/patches/react-native-gesture-handler+2.19.0.patch @@ -1,7 +1,7 @@ diff --git a/node_modules/react-native-gesture-handler/android/build.gradle b/node_modules/react-native-gesture-handler/android/build.gradle --- a/node_modules/react-native-gesture-handler/android/build.gradle +++ b/node_modules/react-native-gesture-handler/android/build.gradle -@@ -229,9 +229,10 @@ +@@ -178,9 +178,10 @@ } def kotlin_version = safeExtGet('kotlinVersion', project.properties['RNGH_kotlinVersion']) @@ -10,6 +10,6 @@ diff --git a/node_modules/react-native-gesture-handler/android/build.gradle b/no dependencies { - implementation 'com.facebook.react:react-native:+' // from node_modules + implementation reactNativeDependency - + if (shouldUseCommonInterfaceFromReanimated()) {