@multiplatform.one/gpg 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cjs/GpgButton.cjs +56 -0
  3. package/dist/cjs/GpgButton.native.js +78 -0
  4. package/dist/cjs/GpgButton.native.js.map +1 -0
  5. package/dist/cjs/GpgContext.cjs +37 -0
  6. package/dist/cjs/GpgContext.native.js +40 -0
  7. package/dist/cjs/GpgContext.native.js.map +1 -0
  8. package/dist/cjs/GpgProvider.cjs +112 -0
  9. package/dist/cjs/GpgProvider.native.js +127 -0
  10. package/dist/cjs/GpgProvider.native.js.map +1 -0
  11. package/dist/cjs/SignInWithGpg.cjs +132 -0
  12. package/dist/cjs/SignInWithGpg.native.js +159 -0
  13. package/dist/cjs/SignInWithGpg.native.js.map +1 -0
  14. package/dist/cjs/index.cjs +54 -0
  15. package/dist/cjs/index.native.js +57 -0
  16. package/dist/cjs/index.native.js.map +1 -0
  17. package/dist/cjs/screens.cjs +70 -0
  18. package/dist/cjs/screens.native.js +79 -0
  19. package/dist/cjs/screens.native.js.map +1 -0
  20. package/dist/cjs/useGpg.cjs +70 -0
  21. package/dist/cjs/useGpg.native.js +77 -0
  22. package/dist/cjs/useGpg.native.js.map +1 -0
  23. package/dist/esm/GpgButton.mjs +30 -0
  24. package/dist/esm/GpgButton.mjs.map +1 -0
  25. package/dist/esm/GpgButton.native.js +38 -0
  26. package/dist/esm/GpgButton.native.js.map +1 -0
  27. package/dist/esm/GpgContext.mjs +11 -0
  28. package/dist/esm/GpgContext.mjs.map +1 -0
  29. package/dist/esm/GpgContext.native.js +11 -0
  30. package/dist/esm/GpgContext.native.js.map +1 -0
  31. package/dist/esm/GpgProvider.mjs +86 -0
  32. package/dist/esm/GpgProvider.mjs.map +1 -0
  33. package/dist/esm/GpgProvider.native.js +98 -0
  34. package/dist/esm/GpgProvider.native.js.map +1 -0
  35. package/dist/esm/SignInWithGpg.mjs +107 -0
  36. package/dist/esm/SignInWithGpg.mjs.map +1 -0
  37. package/dist/esm/SignInWithGpg.native.js +120 -0
  38. package/dist/esm/SignInWithGpg.native.js.map +1 -0
  39. package/dist/esm/index.mjs +8 -0
  40. package/dist/esm/index.mjs.map +1 -0
  41. package/dist/esm/index.native.js +8 -0
  42. package/dist/esm/index.native.js.map +1 -0
  43. package/dist/esm/screens.mjs +32 -0
  44. package/dist/esm/screens.mjs.map +1 -0
  45. package/dist/esm/screens.native.js +38 -0
  46. package/dist/esm/screens.native.js.map +1 -0
  47. package/dist/esm/useGpg.mjs +45 -0
  48. package/dist/esm/useGpg.mjs.map +1 -0
  49. package/dist/esm/useGpg.native.js +49 -0
  50. package/dist/esm/useGpg.native.js.map +1 -0
  51. package/package.json +73 -0
  52. package/src/GpgButton.tsx +37 -0
  53. package/src/GpgContext.tsx +49 -0
  54. package/src/GpgProvider.tsx +126 -0
  55. package/src/SignInWithGpg.tsx +101 -0
  56. package/src/index.ts +14 -0
  57. package/src/screens.tsx +50 -0
  58. package/src/useGpg.ts +57 -0
@@ -0,0 +1,8 @@
1
+ import { useGpg } from "./useGpg.native.js";
2
+ import { GpgProvider, GPG_NONCE_QUERY_KEY } from "./GpgProvider.native.js";
3
+ import { GpgContext, GpgStatusState } from "./GpgContext.native.js";
4
+ import { default as default2 } from "./SignInWithGpg.native.js";
5
+ import { default as default3, GPGButton } from "./GpgButton.native.js";
6
+ import { gpgScreen, GpgScreenRegistrar, GpgBridge } from "./screens.native.js";
7
+ export { GPGButton, GPG_NONCE_QUERY_KEY, GpgBridge, GpgContext, GpgProvider, GpgScreenRegistrar, GpgStatusState, default2 as SignInWithGpg, default3 as SignInWithGpgButton, gpgScreen, useGpg };
8
+ //# sourceMappingURL=index.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useGpg","GpgProvider","GPG_NONCE_QUERY_KEY","GpgContext","GpgStatusState","default","default2","default3","GPGButton","gpgScreen","GpgScreenRegistrar","GpgBridge"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAKA,SAASA,MAAA,QAAc;AACvB,SAASC,WAAA,EAAaC,mBAAA,QAA2B;AACjD,SAASC,UAAA,EAAYC,cAAA,QAAsB;AAG3C,SAAoBC,OAAA,IAAXC,QAAA,QAAgC;AACzC,SAAoBD,OAAA,IAAXE,QAAA,EAAgCC,SAAA,QAAiB;AAE1D,SAASC,SAAA,EAAWC,kBAAA,EAAoBC,SAAA,QAAiB","ignoreList":[]}
@@ -0,0 +1,32 @@
1
+ import { ModalStateProvider, routes, useRegisterScreens } from "@multiplatform.one/connectkit";
2
+ import { useContext as useReactContext } from "react";
3
+ import SignInWithGpg from "./SignInWithGpg.mjs";
4
+ import { GpgContext } from "./GpgContext.mjs";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ const gpgScreen = {
7
+ id: routes.SIGNINWITHGPG,
8
+ component: SignInWithGpg,
9
+ heading: "Sign in with GPG",
10
+ availableWhen: () => true,
11
+ showBack: false,
12
+ showInfo: false
13
+ };
14
+ const GpgScreenRegistrar = () => {
15
+ useRegisterScreens([gpgScreen]);
16
+ return null;
17
+ };
18
+ const GpgBridge = ({
19
+ children
20
+ }) => {
21
+ const signInWithGpg = useReactContext(GpgContext)?.enabled ?? true;
22
+ return /* @__PURE__ */jsxs(Fragment, {
23
+ children: [/* @__PURE__ */jsx(GpgScreenRegistrar, {}), /* @__PURE__ */jsx(ModalStateProvider, {
24
+ value: {
25
+ signInWithGpg
26
+ },
27
+ children
28
+ })]
29
+ });
30
+ };
31
+ export { GpgBridge, GpgScreenRegistrar, gpgScreen };
32
+ //# sourceMappingURL=screens.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ModalStateProvider","routes","useRegisterScreens","useContext","useReactContext","SignInWithGpg","GpgContext","Fragment","jsx","jsxs","gpgScreen","id","SIGNINWITHGPG","component","heading","availableWhen","showBack","showInfo","GpgScreenRegistrar","GpgBridge","children","signInWithGpg","enabled","value"],"sources":["../../src/screens.tsx"],"sourcesContent":[null],"mappings":"AACA,SACEA,kBAAA,EACAC,MAAA,EACAC,kBAAA,QAGK;AACP,SAASC,UAAA,IAAcC,eAAA,QAAuB;AAC9C,OAAOC,aAAA,MAAmB;AAC1B,SAASC,UAAA,QAAkB;AAkCvB,SAAAC,QAAA,EACEC,GAAA,EADFC,IAAA;AA9BG,MAAMC,SAAA,GAAuB;EAClCC,EAAA,EAAIV,MAAA,CAAOW,aAAA;EACXC,SAAA,EAAWR,aAAA;EACXS,OAAA,EAAS;EACTC,aAAA,EAAeA,CAAA,KAAM;EACrBC,QAAA,EAAU;EACVC,QAAA,EAAU;AACZ;AAOO,MAAMC,kBAAA,GAA+BA,CAAA,KAAM;EAChDhB,kBAAA,CAAmB,CAACQ,SAAS,CAAC;EAC9B,OAAO;AACT;AAQO,MAAMS,SAAA,GAA6EA,CAAC;EACzFC;AACF,MAAM;EACJ,MAAMC,aAAA,GAAgBjB,eAAA,CAAgBE,UAAU,GAAGgB,OAAA,IAAW;EAC9D,OACE,eAAAb,IAAA,CAAAF,QAAA;IACEa,QAAA,kBAAAZ,GAAA,CAACU,kBAAA,IAAmB,GACpB,eAAAV,GAAA,CAACR,kBAAA;MAAmBuB,KAAA,EAAO;QAAEF;MAAc;MAAID;IAAA,CAAS;EAAA,CAC1D;AAEJ","ignoreList":[]}
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { ModalStateProvider, routes, useRegisterScreens } from "@multiplatform.one/connectkit";
4
+ import { useContext as useReactContext } from "react";
5
+ import SignInWithGpg from "./SignInWithGpg.native.js";
6
+ import { GpgContext } from "./GpgContext.native.js";
7
+ var gpgScreen = {
8
+ id: routes.SIGNINWITHGPG,
9
+ component: SignInWithGpg,
10
+ heading: "Sign in with GPG",
11
+ availableWhen: function availableWhen() {
12
+ return true;
13
+ },
14
+ showBack: false,
15
+ showInfo: false
16
+ };
17
+ var GpgScreenRegistrar = function GpgScreenRegistrar2() {
18
+ useRegisterScreens([gpgScreen]);
19
+ return null;
20
+ };
21
+ var GpgBridge = function GpgBridge2(param) {
22
+ var {
23
+ children
24
+ } = param;
25
+ var _ref;
26
+ var _useReactContext;
27
+ var signInWithGpg = (_ref = (_useReactContext = useReactContext(GpgContext)) === null || _useReactContext === void 0 ? void 0 : _useReactContext.enabled) !== null && _ref !== void 0 ? _ref : true;
28
+ return /* @__PURE__ */_jsxs(_Fragment, {
29
+ children: [/* @__PURE__ */_jsx(GpgScreenRegistrar, {}), /* @__PURE__ */_jsx(ModalStateProvider, {
30
+ value: {
31
+ signInWithGpg
32
+ },
33
+ children
34
+ })]
35
+ });
36
+ };
37
+ export { GpgBridge, GpgScreenRegistrar, gpgScreen };
38
+ //# sourceMappingURL=screens.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["jsx","_jsx","jsxs","_jsxs","Fragment","_Fragment","React","ModalStateProvider","routes","useRegisterScreens","useContext","useReactContext","SignInWithGpg","GpgContext","gpgScreen","id","SIGNINWITHGPG","component","heading","availableWhen","showBack","showInfo","GpgScreenRegistrar","GpgScreenRegistrar2","GpgBridge","GpgBridge2","param","children","_ref","_useReactContext","signInWithGpg","enabled"],"sources":["../../src/screens.tsx"],"sourcesContent":[null],"mappings":"AACA,SAAAA,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA,EAAAC,QAAA,IAAAC,SAAA;AAAA,OACEC,KAAA;AAAA,SACAC,kBAAA,EAAAC,MAAA,EAAAC,kBAAA;AAAA,SACAC,UAAA,IAAAC,eAAA;AAAA,OAGKC,aAAA;AACP,SAASC,UAAA,QAAc;AACvB,IAAAC,SAAO;EACPC,EAAA,EAAAP,MAAS,CAAAQ,aAAA;EAkCLC,SAAA,EAAAL,aAAA;EA9BGM,OAAM,oBAAuB;EAClCC,aAAW,WAAAA,cAAA;IACX,WAAW;EACX;EACAC,QAAA,OAAe;EACfC,QAAA,EAAU;AAAA;AAEZ,IAAAC,kBAAA,YAAAC,oBAAA;EAOOd,kBAAM,EACXK,SAAA,CACA;EACF;AAQO;AAAoF,IACzFU,SAAA,YAAAC,WAAAC,KAAA;EACF,IAAM;IAAAC;EAAA,IAAAD,KAAA;EACJ,IAAAE,IAAM;EACN,IAAAC,gBACE;EACE,IAAAC,aAAA,IAAAF,IAAC,IAAAC,gBAAmB,GAAAlB,eAAA,CAAAE,UAAA,eAAAgB,gBAAA,uBAAAA,gBAAA,CAAAE,OAAA,cAAAH,IAAA,cAAAA,IAAA;EAAA,OACpB,eAACzB,KAAA,CAAAE,SAAA;IAAuDsB,QAC1D,GAEJ,eAAA1B,IAAA,CAAAqB,kBAAA,O","ignoreList":[]}
@@ -0,0 +1,45 @@
1
+ import { useContext } from "react";
2
+ import { GpgContext, GpgStatusState } from "./GpgContext.mjs";
3
+ const useGpg = () => {
4
+ const gpg = useContext(GpgContext);
5
+ if (!gpg) {
6
+ return {
7
+ message: null,
8
+ command: null,
9
+ status: GpgStatusState.ERROR,
10
+ error: new Error("useGpg hook must be inside a GpgProvider."),
11
+ isLoading: false,
12
+ isSuccess: false,
13
+ isError: true,
14
+ isReady: false,
15
+ signIn: () => Promise.reject(),
16
+ reset: () => {}
17
+ };
18
+ }
19
+ const {
20
+ nonce,
21
+ message,
22
+ command,
23
+ status,
24
+ signIn,
25
+ resetStatus
26
+ } = gpg;
27
+ const isLoading = status === GpgStatusState.LOADING || nonce.isLoading;
28
+ const isSuccess = status === GpgStatusState.SUCCESS;
29
+ const isError = status === GpgStatusState.ERROR;
30
+ const isReady = !!message && !isLoading;
31
+ return {
32
+ message,
33
+ command,
34
+ status,
35
+ error: nonce.error,
36
+ isLoading,
37
+ isSuccess,
38
+ isError,
39
+ isReady,
40
+ signIn,
41
+ reset: () => resetStatus()
42
+ };
43
+ };
44
+ export { useGpg };
45
+ //# sourceMappingURL=useGpg.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useContext","GpgContext","GpgStatusState","useGpg","gpg","message","command","status","ERROR","error","Error","isLoading","isSuccess","isError","isReady","signIn","Promise","reject","reset","nonce","resetStatus","LOADING","SUCCESS"],"sources":["../../src/useGpg.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,UAAA,QAAkB;AAC3B,SAASC,UAAA,EAAYC,cAAA,QAAsB;AAoBpC,MAAMC,MAAA,GAASA,CAAA,KAAuB;EAC3C,MAAMC,GAAA,GAAMJ,UAAA,CAAWC,UAAU;EACjC,IAAI,CAACG,GAAA,EAAK;IACR,OAAO;MACLC,OAAA,EAAS;MACTC,OAAA,EAAS;MACTC,MAAA,EAAQL,cAAA,CAAeM,KAAA;MACvBC,KAAA,EAAO,IAAIC,KAAA,CAAM,2CAA2C;MAC5DC,SAAA,EAAW;MACXC,SAAA,EAAW;MACXC,OAAA,EAAS;MACTC,OAAA,EAAS;MACTC,MAAA,EAAQA,CAAA,KAAMC,OAAA,CAAQC,MAAA,CAAO;MAC7BC,KAAA,EAAOA,CAAA,KAAM,CAAC;IAChB;EACF;EAEA,MAAM;IAAEC,KAAA;IAAOd,OAAA;IAASC,OAAA;IAASC,MAAA;IAAQQ,MAAA;IAAQK;EAAY,IAAIhB,GAAA;EACjE,MAAMO,SAAA,GAAYJ,MAAA,KAAWL,cAAA,CAAemB,OAAA,IAAWF,KAAA,CAAMR,SAAA;EAC7D,MAAMC,SAAA,GAAYL,MAAA,KAAWL,cAAA,CAAeoB,OAAA;EAC5C,MAAMT,OAAA,GAAUN,MAAA,KAAWL,cAAA,CAAeM,KAAA;EAC1C,MAAMM,OAAA,GAAU,CAAC,CAACT,OAAA,IAAW,CAACM,SAAA;EAE9B,OAAO;IACLN,OAAA;IACAC,OAAA;IACAC,MAAA;IACAE,KAAA,EAAOU,KAAA,CAAMV,KAAA;IACbE,SAAA;IACAC,SAAA;IACAC,OAAA;IACAC,OAAA;IACAC,MAAA;IACAG,KAAA,EAAOA,CAAA,KAAME,WAAA,CAAY;EAC3B;AACF","ignoreList":[]}
@@ -0,0 +1,49 @@
1
+ import { useContext } from "react";
2
+ import { GpgContext, GpgStatusState } from "./GpgContext.native.js";
3
+ var useGpg = function useGpg2() {
4
+ var gpg = useContext(GpgContext);
5
+ if (!gpg) {
6
+ return {
7
+ message: null,
8
+ command: null,
9
+ status: GpgStatusState.ERROR,
10
+ error: new Error("useGpg hook must be inside a GpgProvider."),
11
+ isLoading: false,
12
+ isSuccess: false,
13
+ isError: true,
14
+ isReady: false,
15
+ signIn: function signIn2() {
16
+ return Promise.reject();
17
+ },
18
+ reset: function reset() {}
19
+ };
20
+ }
21
+ var {
22
+ nonce,
23
+ message,
24
+ command,
25
+ status,
26
+ signIn,
27
+ resetStatus
28
+ } = gpg;
29
+ var isLoading = status === GpgStatusState.LOADING || nonce.isLoading;
30
+ var isSuccess = status === GpgStatusState.SUCCESS;
31
+ var isError = status === GpgStatusState.ERROR;
32
+ var isReady = !!message && !isLoading;
33
+ return {
34
+ message,
35
+ command,
36
+ status,
37
+ error: nonce.error,
38
+ isLoading,
39
+ isSuccess,
40
+ isError,
41
+ isReady,
42
+ signIn,
43
+ reset: function reset() {
44
+ return resetStatus();
45
+ }
46
+ };
47
+ };
48
+ export { useGpg };
49
+ //# sourceMappingURL=useGpg.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useContext","GpgContext","GpgStatusState","useGpg","useGpg2","gpg","message","command","status","ERROR","error","Error","isLoading","isSuccess","isError","isReady","signIn","signIn2","Promise","reject","reset","nonce","resetStatus","LOADING","SUCCESS"],"sources":["../../src/useGpg.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,UAAA,QAAkB;AAC3B,SAASC,UAAA,EAAYC,cAAA,QAAsB;AAoBpC,IAAAC,MAAM,YAAgCC,QAAA;EAC3C,IAAAC,GAAM,GAAAL,UAAM,CAAAC,UAAW;EACvB,IAAI,CAACI,GAAA,EAAK;IACR,OAAO;MACLC,OAAA,EAAS;MACTC,OAAA,EAAS;MACTC,MAAA,EAAQN,cAAA,CAAeO,KAAA;MACvBC,KAAA,EAAO,IAAIC,KAAA,CAAM,2CAA2C;MAC5DC,SAAA,EAAW;MACXC,SAAA,EAAW;MACXC,OAAA,EAAS;MACTC,OAAA,EAAS;MACTC,MAAA,EAAQ,SAAMC,OAAQA,CAAA;QACtB,OAAOC,OAAM,CAAAC,MAAA;MAAC;MAChBC,KAAA,WAAAA,MAAA,GACF;IAEA;EACA;EACA;IAAMC,KAAA;IAAAf,OAAY;IAAAC,OAAW;IAAAC,MAAA;IAAAQ,MAAe;IAAAM;EAAA,IAAAjB,GAAA;EAC5C,IAAAO,SAAM,GAAUJ,MAAA,KAAWN,cAAA,CAAeqB,OAAA,IAAAF,KAAA,CAAAT,SAAA;EAC1C,IAAAC,SAAM,GAAUL,MAAE,KAAAN,cAAY,CAAAsB,OAAA;EAE9B,IAAAV,OAAO,GAAAN,MAAA,KAAAN,cAAA,CAAAO,KAAA;EAAA,IACLM,OAAA,KAAAT,OAAA,KAAAM,SAAA;EAAA,OACA;IACAN,OAAA;IACAC,OAAO;IACPC,MAAA;IACAE,KAAA,EAAAW,KAAA,CAAAX,KAAA;IACAE,SAAA;IACAC,SAAA;IACAC,OAAA;IACAC,OAAO;IACTC,MAAA;IACFI,KAAA,WAAAA,MAAA","ignoreList":[]}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@multiplatform.one/gpg",
3
+ "version": "6.0.0",
4
+ "description": "Sign-In With GPG (SIWG) screen + GpgProvider for the connectkit modal — no viem",
5
+ "keywords": [
6
+ "connectkit",
7
+ "cross-platform",
8
+ "gpg",
9
+ "multiplatform",
10
+ "react",
11
+ "react-native",
12
+ "sign-in",
13
+ "siwg",
14
+ "tamagui"
15
+ ],
16
+ "homepage": "https://multiplatform.one",
17
+ "bugs": {
18
+ "url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one/issues",
19
+ "email": "support@risserlabs.com"
20
+ },
21
+ "license": "Apache-2.0",
22
+ "author": "BitSpur <support@risserlabs.com> (https://risserlabs.com)",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git"
26
+ },
27
+ "source": "src/index.ts",
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "types"
32
+ ],
33
+ "sideEffects": false,
34
+ "main": "src/index.ts",
35
+ "module": "src/index.ts",
36
+ "types": "src/index.ts",
37
+ "exports": {
38
+ "./package.json": "./package.json",
39
+ ".": {
40
+ "source": "./src/index.ts",
41
+ "react-native": {
42
+ "types": "./src/index.ts",
43
+ "import": "./src/index.ts",
44
+ "require": "./src/index.ts",
45
+ "default": "./src/index.ts"
46
+ },
47
+ "types": "./src/index.ts",
48
+ "import": "./dist/esm/index.mjs",
49
+ "require": "./dist/cjs/index.cjs"
50
+ }
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "dependencies": {
56
+ "@multiplatform.one/connectkit": "6.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@tamagui/build": "2.0.0-rc.41",
60
+ "@types/react": "~19.2.14",
61
+ "react": "19.2.5",
62
+ "typescript": "~5.9.3",
63
+ "@multiplatform.one/config": "6.0.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@tanstack/react-query": "^5.62.0",
67
+ "react": "^19.1.0"
68
+ },
69
+ "scripts": {
70
+ "typecheck": "tsc --noEmit",
71
+ "build": "rm -rf dist types 2>/dev/null && tamagui-build --ts-project ./tsconfig.build.json && node scripts/fix-dist-specifiers.mjs"
72
+ }
73
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { Button, ResetContainer, useModal } from "@multiplatform.one/connectkit";
3
+
4
+ // Standalone trigger for the GPG flow. GPG is NOT a wallet connector — there is nothing
5
+ // to connect first — so this button simply opens the SAME modal straight to the GPG
6
+ // screen (no connect step), mirroring how the SIWE button opens its screen.
7
+ type ButtonProps = {
8
+ children?: React.ReactNode;
9
+ };
10
+
11
+ export const GPGButton: React.FC<ButtonProps> = ({ children }) => {
12
+ // GPG is connection-free: open the SAME modal straight to its screen via the
13
+ // generic registry opener (gpg registers the 'signInWithGpg' screen).
14
+ const { openScreen } = useModal();
15
+ return (
16
+ <Button
17
+ key="button"
18
+ style={{ margin: 0 }}
19
+ arrow
20
+ // preventDefault so we never submit a surrounding host <form> (e.g. keycloak's
21
+ // login form) — the underlying styled button can render as type=submit.
22
+ onClick={(e?: { preventDefault?: () => void }) => {
23
+ e?.preventDefault?.();
24
+ openScreen("signInWithGpg");
25
+ }}
26
+ >
27
+ {children ?? "Sign in with GPG"}
28
+ </Button>
29
+ );
30
+ };
31
+
32
+ const GPGButtonComponent: React.FC<ButtonProps> = ({ ...props }) => (
33
+ <ResetContainer>
34
+ <GPGButton {...props} />
35
+ </ResetContainer>
36
+ );
37
+ export default GPGButtonComponent;
@@ -0,0 +1,49 @@
1
+ import { createContext } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+
4
+ // Sign-In With GPG (SIWG) is a SEPARATE, connection-free signer: unlike a wallet
5
+ // there is nothing to "connect" first — the user signs a challenge in their terminal
6
+ // and pastes the result back. This module deliberately pulls in NO wagmi/viem, so it
7
+ // can later move into a standalone `@multiplatform.one/gpg` package on top of connectkit.
8
+
9
+ export enum GpgStatusState {
10
+ READY = "ready",
11
+ LOADING = "loading",
12
+ SUCCESS = "success",
13
+ ERROR = "error",
14
+ }
15
+
16
+ export type GpgConfig = {
17
+ // Required: fetch a fresh nonce from the backend (e.g. keycloak /realms/<realm>/siww/nonce).
18
+ getNonce: () => Promise<string>;
19
+ // Required: submit the signed credential. The screen produces
20
+ // username = base64(armoredSignature + armoredPublicKey)
21
+ // password = base64(message)
22
+ // For a keycloak browser login this writes the login form and submits it (the page
23
+ // then navigates); an API consumer may POST and return a boolean instead.
24
+ onSign: (cred: {
25
+ username: string;
26
+ password: string;
27
+ message: string;
28
+ }) => Promise<boolean | void> | boolean | void;
29
+
30
+ // Optional: override the challenge message (default = the standard SIWG message).
31
+ buildMessage?: (args: { nonce: string }) => string;
32
+ enabled?: boolean;
33
+ nonceRefetchInterval?: number;
34
+ };
35
+
36
+ export type GpgContextValue = Required<
37
+ Pick<GpgConfig, "getNonce" | "onSign" | "enabled" | "nonceRefetchInterval">
38
+ > & {
39
+ buildMessage?: GpgConfig["buildMessage"];
40
+ nonce: ReturnType<typeof useQuery<string | null>>;
41
+ // Derived from the nonce — what the user copies/signs, and the ready-to-run command.
42
+ message: string | null;
43
+ command: string | null;
44
+ status: GpgStatusState;
45
+ signIn: (pastedBlob: string) => Promise<boolean>;
46
+ resetStatus: () => void;
47
+ };
48
+
49
+ export const GpgContext = createContext<GpgContextValue | null>(null);
@@ -0,0 +1,126 @@
1
+ import { ReactNode, useContext, useMemo, useState } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+
4
+ import { Context as ConnectKitContext } from "@multiplatform.one/connectkit";
5
+ import { GpgContext, GpgConfig, GpgStatusState } from "./GpgContext";
6
+
7
+ type Props = GpgConfig & {
8
+ children: ReactNode;
9
+ onSignIn?: () => void;
10
+ };
11
+
12
+ export const GPG_NONCE_QUERY_KEY = "ckGpgNonce";
13
+
14
+ // UTF-8-safe base64 — the server decodes with new String(decoder.decode(...), UTF_8),
15
+ // so plain btoa would corrupt non-ASCII. Must keep the unescape(encodeURIComponent()).
16
+ const b64 = (s: string) => window.btoa(unescape(encodeURIComponent(s)));
17
+
18
+ // The default SIWG challenge. Only the header line (domain) and the Nonce line are
19
+ // load-bearing per the server's GpgMessage grammar; the rest is a cosmetic envelope.
20
+ function defaultBuildMessage({ nonce }: { nonce: string }): string {
21
+ return (
22
+ `${window.location.host} wants you to sign in with your GPG key.\n\n` +
23
+ `URI: ${window.location.origin}\n` +
24
+ `Version: 1\n` +
25
+ `Nonce: ${nonce}\n` +
26
+ `Issued At: ${new Date().toISOString()}`
27
+ );
28
+ }
29
+
30
+ export const GpgProvider = ({
31
+ children,
32
+ enabled = true,
33
+ nonceRefetchInterval = 1000 * 60 * 5,
34
+ buildMessage,
35
+ onSignIn,
36
+ ...gpgConfig
37
+ }: Props) => {
38
+ const [status, setStatus] = useState<GpgStatusState>(GpgStatusState.READY);
39
+ const resetStatus = () => setStatus(GpgStatusState.READY);
40
+
41
+ // Mirror SIWEProvider's guards: single instance, and mounted OUTSIDE ConnectKitProvider
42
+ // so the modal's GPG screen can read this context.
43
+ if (useContext(GpgContext)) {
44
+ throw new Error("Multiple, nested usages of GpgProvider detected. Please use only one.");
45
+ }
46
+ if (useContext(ConnectKitContext)) {
47
+ throw new Error("ConnectKitProvider must be mounted inside GpgProvider.");
48
+ }
49
+
50
+ const nonce = useQuery({
51
+ queryKey: [GPG_NONCE_QUERY_KEY],
52
+ queryFn: () => gpgConfig.getNonce(),
53
+ refetchInterval: nonceRefetchInterval,
54
+ });
55
+
56
+ // Compute the challenge ONCE per nonce (it embeds an Issued At timestamp); recomputing
57
+ // on every render would desync the displayed/copied message from the one base64'd into
58
+ // `password`, breaking the signature (which is verified over the exact message bytes).
59
+ const message = useMemo(
60
+ () => (nonce.data ? (buildMessage ?? defaultBuildMessage)({ nonce: nonce.data }) : null),
61
+ [nonce.data, buildMessage],
62
+ );
63
+ // The ready-to-run terminal command: detached armored signature + the exported public
64
+ // key, concatenated — exactly the blob the user pastes back. printf %s (no newline) so
65
+ // the signed bytes are byte-identical to `message`.
66
+ const command = useMemo(
67
+ () =>
68
+ message
69
+ ? `{ printf %s '${message}' | gpg --detach-sign --armor; gpg --armor --export; }`
70
+ : null,
71
+ [message],
72
+ );
73
+
74
+ const signIn = async (pastedBlob: string): Promise<boolean> => {
75
+ const blob = pastedBlob.trim();
76
+ if (
77
+ !blob.includes("-----BEGIN PGP SIGNATURE-----") ||
78
+ !blob.includes("-----BEGIN PGP PUBLIC KEY BLOCK-----")
79
+ ) {
80
+ setStatus(GpgStatusState.ERROR);
81
+ throw new Error(
82
+ "Paste the FULL output — it must contain both a PGP SIGNATURE and a PUBLIC KEY block.",
83
+ );
84
+ }
85
+ if (!message) {
86
+ setStatus(GpgStatusState.ERROR);
87
+ throw new Error("Could not fetch nonce");
88
+ }
89
+ try {
90
+ setStatus(GpgStatusState.LOADING);
91
+ // username = base64(signature + public key), password = base64(message).
92
+ const result = await gpgConfig.onSign({
93
+ username: b64(blob),
94
+ password: b64(message),
95
+ message,
96
+ });
97
+ // A keycloak browser login navigates away here; an API consumer returns a boolean.
98
+ if (result === false) throw new Error("GPG sign-in was rejected");
99
+ setStatus(GpgStatusState.SUCCESS);
100
+ onSignIn?.();
101
+ return true;
102
+ } catch (error) {
103
+ setStatus(GpgStatusState.ERROR);
104
+ throw error;
105
+ }
106
+ };
107
+
108
+ return (
109
+ <GpgContext.Provider
110
+ value={{
111
+ enabled,
112
+ nonceRefetchInterval,
113
+ buildMessage,
114
+ ...gpgConfig,
115
+ nonce,
116
+ message,
117
+ command,
118
+ status,
119
+ signIn,
120
+ resetStatus,
121
+ }}
122
+ >
123
+ {children}
124
+ </GpgContext.Provider>
125
+ );
126
+ };
@@ -0,0 +1,101 @@
1
+ import React, { CSSProperties, useState } from "react";
2
+
3
+ import {
4
+ useContext,
5
+ PageContent,
6
+ ModalBody,
7
+ ModalContent,
8
+ Button,
9
+ CopyToClipboard,
10
+ } from "@multiplatform.one/connectkit";
11
+ import { useGpg } from "./useGpg";
12
+
13
+ // Sign-In With GPG screen: a connection-free, copy/paste flow rendered inside the SAME
14
+ // ConnectKit modal as the wallet screens. Reads everything from useGpg() (the consumer's
15
+ // GpgProvider supplies the backend-specific getNonce/onSign) — no keycloak knowledge here.
16
+ const textarea: CSSProperties = {
17
+ width: "100%",
18
+ boxSizing: "border-box",
19
+ fontFamily: "var(--ck-font-family, monospace)",
20
+ fontSize: 11,
21
+ lineHeight: 1.4,
22
+ padding: 8,
23
+ borderRadius: "var(--ck-primary-button-border-radius, 12px)",
24
+ border: "1px solid var(--ck-body-divider, #d3d3d3)",
25
+ background: "var(--ck-body-background-secondary, #f6f7f9)",
26
+ color: "var(--ck-body-color, inherit)",
27
+ resize: "vertical",
28
+ };
29
+ const label: CSSProperties = {
30
+ textAlign: "left",
31
+ fontSize: 14,
32
+ fontWeight: 500,
33
+ margin: "4px 0",
34
+ };
35
+
36
+ const SignInWithGpg: React.FC = () => {
37
+ const context = useContext();
38
+ const { message, command, signIn, isLoading, isReady } = useGpg();
39
+ const [pasted, setPasted] = useState("");
40
+ const [error, setError] = useState<string | null>(null);
41
+ const [busy, setBusy] = useState(false);
42
+
43
+ const onSubmit = async () => {
44
+ setError(null);
45
+ setBusy(true);
46
+ try {
47
+ await signIn(pasted);
48
+ // A keycloak browser login navigates away; for API consumers, close the modal.
49
+ setTimeout(() => context.setOpen(false), 1000);
50
+ } catch (e) {
51
+ setError(e instanceof Error ? e.message : "GPG sign-in failed");
52
+ setBusy(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <PageContent style={{ width: 320 }}>
58
+ <ModalContent style={{ paddingBottom: 8, gap: 6 }}>
59
+ <ModalBody style={label}>1. Sign the challenge in your terminal:</ModalBody>
60
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
61
+ <CopyToClipboard variant="button" string={command ?? ""}>
62
+ Copy gpg command
63
+ </CopyToClipboard>
64
+ <CopyToClipboard variant="button" string={message ?? ""}>
65
+ Copy message only
66
+ </CopyToClipboard>
67
+ </div>
68
+ <textarea
69
+ readOnly
70
+ rows={4}
71
+ value={message ?? (isLoading ? "Loading challenge…" : "")}
72
+ style={textarea}
73
+ />
74
+ <ModalBody style={label}>2. Paste the full output below:</ModalBody>
75
+ <textarea
76
+ rows={6}
77
+ value={pasted}
78
+ onChange={(e) => setPasted(e.target.value)}
79
+ placeholder="-----BEGIN PGP SIGNATURE----- … and your -----BEGIN PGP PUBLIC KEY BLOCK-----"
80
+ style={textarea}
81
+ />
82
+ <Button onClick={onSubmit} disabled={!isReady || !pasted || busy} waiting={busy} arrow>
83
+ Sign in
84
+ </Button>
85
+ {error && (
86
+ <ModalBody
87
+ style={{
88
+ color: "var(--ck-body-color-danger, #c9190b)",
89
+ fontSize: 13,
90
+ textAlign: "left",
91
+ }}
92
+ >
93
+ {error}
94
+ </ModalBody>
95
+ )}
96
+ </ModalContent>
97
+ </PageContent>
98
+ );
99
+ };
100
+
101
+ export default SignInWithGpg;
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ // @multiplatform.one/gpg — Sign-In With GPG (SIWG) for the connectkit modal.
2
+ //
3
+ // A connection-free signer: the user signs a challenge in their terminal and pastes
4
+ // the result back. Pulls in NO wagmi/viem — depends only on @multiplatform.one/connectkit
5
+ // — so a GPG-only app can install it without the wallet stack.
6
+ export { useGpg } from "./useGpg";
7
+ export { GpgProvider, GPG_NONCE_QUERY_KEY } from "./GpgProvider";
8
+ export { GpgContext, GpgStatusState } from "./GpgContext";
9
+ export type { GpgConfig, GpgContextValue } from "./GpgContext";
10
+
11
+ export { default as SignInWithGpg } from "./SignInWithGpg";
12
+ export { default as SignInWithGpgButton, GPGButton } from "./GpgButton";
13
+
14
+ export { gpgScreen, GpgScreenRegistrar, GpgBridge } from "./screens";
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import {
3
+ ModalStateProvider,
4
+ routes,
5
+ useRegisterScreens,
6
+ type ConnectCallbackProps,
7
+ type ScreenDef,
8
+ } from "@multiplatform.one/connectkit";
9
+ import { useContext as useReactContext } from "react";
10
+ import SignInWithGpg from "./SignInWithGpg";
11
+ import { GpgContext } from "./GpgContext";
12
+
13
+ // The GPG screen contributed to connectkit's signer-agnostic modal. GPG has no
14
+ // connect step, so it's available in both connected and disconnected states.
15
+ export const gpgScreen: ScreenDef = {
16
+ id: routes.SIGNINWITHGPG,
17
+ component: SignInWithGpg,
18
+ heading: "Sign in with GPG",
19
+ availableWhen: () => true,
20
+ showBack: false,
21
+ showInfo: false,
22
+ };
23
+
24
+ /**
25
+ * Mount inside ConnectKitProvider (under ScreenRegistryProvider) to publish the GPG
26
+ * screen to the modal. Renders nothing. web3 mounts this alongside its wallet
27
+ * screens; a gpg-only app uses {@link GpgBridge}.
28
+ */
29
+ export const GpgScreenRegistrar: React.FC = () => {
30
+ useRegisterScreens([gpgScreen]);
31
+ return null;
32
+ };
33
+
34
+ /**
35
+ * The signer bridge for a GPG-only app (no wagmi/web3). Pass to ConnectKitProvider's
36
+ * `bridge` prop: it registers the GPG screen and injects a ModalState advertising
37
+ * GPG sign-in is available. Open the screen via the SignInWithGpgButton (which calls
38
+ * `openScreen('signInWithGpg')`).
39
+ */
40
+ export const GpgBridge: React.FC<{ children?: React.ReactNode } & ConnectCallbackProps> = ({
41
+ children,
42
+ }) => {
43
+ const signInWithGpg = useReactContext(GpgContext)?.enabled ?? true;
44
+ return (
45
+ <>
46
+ <GpgScreenRegistrar />
47
+ <ModalStateProvider value={{ signInWithGpg }}>{children}</ModalStateProvider>
48
+ </>
49
+ );
50
+ };