diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17e22584..19b5665f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: # tags: ["v*"] env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: Run-Tests: @@ -23,12 +23,12 @@ jobs: fetch-depth: 0 # Needed for the release tag // `git fetch --tags` will also work - name: Setup Java and Scala uses: olafurpg/setup-scala@v14 - # - name: Setup Node - # uses: actions/setup-node@v2 - # with: - # node-version: "16" # or whatever - # - name: Setup Scala.JS - # uses: japgolly/setup-scalajs@v1 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: "lts/*" + - name: Setup Scala.JS + uses: japgolly/setup-scalajs@v1 - name: Cache sbt uses: coursier/cache-action@v6.3 - name: Cache embedmongo @@ -37,8 +37,8 @@ jobs: path: ~/.embedmongo key: ${{ runner.os }}-embedmongo-4.7.0 restore-keys: | - ${{ runner.os }}-embedmongo-4.7.0 + ${{ runner.os }}-embedmongo-4.7.0 ### Compile and TESTS ### - run: sbt -mem 2048 -J-Xmx5120m "test" - # env: - # NODE_OPTIONS: "--openssl-legacy-provider" + env: + NODE_OPTIONS: "--openssl-legacy-provider" diff --git a/README.md b/README.md index 1f44cbf3..37fd793e 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,18 @@ A cloud-based agent that forwards messages to mobile devices. - [WIP] `MediatorCoordination 2.0` - https://didcomm.org/mediator-coordination/2.0 - [DONE] `Pickup 3` - https://didcomm.org/pickup/3.0 - [DONE] `TrustPing 2.0` - https://didcomm.org/trust-ping/2.0/ + +## How to run + +### server + +**Start the server**: + - shell> `docker-compose up mongo` + - sbt> `mediator/reStart` +### webapp + +The webapp/webpage is atm just to show the QRcode with out of band invitation for the Mediator. + +**Compile** - sbt> `webapp / Compile / fastOptJS / webpack` + +**Open the webpage for develop** - open> `file:///.../webapp/index-fastopt.html` diff --git a/build.sbt b/build.sbt index fc9e6997..797b9530 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,7 @@ resolvers ++= Resolver.sonatypeOssRepos("snapshots") inThisBuild( Seq( - scalaVersion := "3.3.0", // Also update docs/publishWebsite.sh and any ref to scala-3.2.2 + scalaVersion := "3.3.0", // Also update docs/publishWebsite.sh and any ref to scala-3.3.0 ) ) @@ -20,8 +20,8 @@ lazy val V = new { // // val scalajsLogging = "1.1.2-SNAPSHOT" //"1.1.2" // // https://mvnrepository.com/artifact/dev.zio/zio -// val zio = "2.0.13" -// val zioJson = "0.4.2" + val zio = "2.0.13" + val zioJson = "0.4.2" // val zioMunitTest = "0.1.1" val zioHttp = "0.0.5" val zioConfig = "4.0.0-RC16" @@ -37,6 +37,12 @@ lazy val V = new { val zioTestSbt = "2.0.15" val zioTestMagnolia = "2.0.15" + // For WEBAPP + val laminar = "15.0.1" + val waypoint = "6.0.0" + val upickle = "3.1.0" + // https://www.npmjs.com/package/material-components-web + val materialComponents = "12.0.0" } /** Dependencies */ @@ -56,9 +62,9 @@ lazy val D = new { // val dom = Def.setting("org.scala-js" %%% "scalajs-dom" % V.scalajsDom) -// val zio = Def.setting("dev.zio" %%% "zio" % V.zio) + val zio = Def.setting("dev.zio" %%% "zio" % V.zio) // val zioStreams = Def.setting("dev.zio" %%% "zio-streams" % V.zio) -// val zioJson = Def.setting("dev.zio" %%% "zio-json" % V.zioJson) + val zioJson = Def.setting("dev.zio" %%% "zio-json" % V.zioJson) val zioHttp = Def.setting("dev.zio" %% "zio-http" % V.zioHttp) val zioConfig = Def.setting("dev.zio" %% "zio-config" % V.zioConfig) @@ -80,6 +86,18 @@ lazy val D = new { val zioTest = Def.setting("dev.zio" %% "zio-test" % V.zioTest % Test) val zioTestSbt = Def.setting("dev.zio" %% "zio-test-sbt" % V.zioTestSbt % Test) val zioTestMagnolia = Def.setting("dev.zio" %% "zio-test-magnolia" % V.zioTestMagnolia % Test) + + // For WEBAPP + val laminar = Def.setting("com.raquo" %%% "laminar" % V.laminar) + val waypoint = Def.setting("com.raquo" %%% "waypoint" % V.waypoint) + val upickle = Def.setting("com.lihaoyi" %%% "upickle" % V.upickle) +} + +/** NPM Dependencies */ +lazy val NPM = new { + val qrcode = Seq("qrcode-generator" -> "1.4.4") + + val materialDesign = Seq("material-components-web" -> V.materialComponents) } inThisBuild( @@ -123,13 +141,13 @@ lazy val scalaJSBundlerConfigure: Project => Project = scalaJSLinkerConfig ~= { _.withSourceMap(false) // disabled because it somehow triggers warnings and errors .withModuleKind(ModuleKind.CommonJSModule) // ModuleKind.ESModule - // must be set to ModuleKind.CommonJSModule in projects where ScalaJSBundler plugin is enabled - .withJSHeader( - """/* FMGP scala-did examples and tool - | * https://github.com/FabioPinheiro/scala-did - | * Copyright: Fabio Pinheiro - fabiomgpinheiro@gmail.com - | */""".stripMargin.trim() + "\n" - ) + // must be set to ModuleKind.CommonJSModule in projects where ScalaJSBundler plugin is enabled + // .withJSHeader( + // """/* FMGP scala-did examples and tool + // | * https://github.com/FabioPinheiro/scala-did + // | * Copyright: Fabio Pinheiro - fabiomgpinheiro@gmail.com + // | */""".stripMargin.trim() + "\n" + // ) } ) // .settings( //TODO https://scalacenter.github.io/scalajs-bundler/reference.html#jsdom @@ -206,9 +224,48 @@ lazy val mediator = project dockerBaseImage := "openjdk:11", ) .settings(Test / parallelExecution := false) + .settings( + // WebScalaJSBundlerPlugin + scalaJSProjects := Seq(webapp), + /** scalaJSPipeline task runs scalaJSDev when isDevMode is true, runs scalaJSProd otherwise. scalaJSProd task runs + * all tasks for production, including Scala.js fullOptJS task and source maps scalaJSDev task runs all tasks for + * development, including Scala.js fastOptJS task and source maps. + */ + Assets / pipelineStages := Seq(scalaJSPipeline), + // pipelineStages ++= Seq(digest, gzip), //Compression - If you serve your Scala.js application from a web server, you should additionally gzip the resulting .js files. + Compile / unmanagedResourceDirectories += baseDirectory.value / "src" / "main" / "extra-resources", + // Compile / unmanagedResourceDirectories += (baseDirectory.value.toPath.getParent.getParent / "docs-build" / "target" / "mdoc").toFile, + // Compile / unmanagedResourceDirectories += (baseDirectory.value.toPath.getParent.getParent / "serviceworker" / "target" / "scala-3.3.0" / "fmgp-serviceworker-fastopt").toFile, + Compile / compile := ((Compile / compile) dependsOn scalaJSPipeline).value, + // Frontend dependency configuration + Assets / WebKeys.packagePrefix := "public/", + Runtime / managedClasspath += (Assets / packageBin).value, + ) + .enablePlugins(WebScalaJSBundlerPlugin) .dependsOn(httpUtils.jvm) // did, didExample, .enablePlugins(JavaAppPackaging, DockerPlugin) +lazy val webapp = project + .in(file("webapp")) + .settings(publish / skip := true) + .settings(Test / test := {}) + .settings(name := "webapp") + .configure(scalaJSBundlerConfigure) + .configure(buildInfoConfigure) + .settings( + libraryDependencies ++= Seq(D.laminar.value, D.waypoint.value, D.upickle.value), + libraryDependencies ++= Seq(D.zio.value, D.zioJson.value), + libraryDependencies ++= Seq(D.scalaDID.value, D.scalaDID_peer.value), + Compile / npmDependencies ++= NPM.qrcode ++ NPM.materialDesign + ) + .settings( + stShortModuleNames := true, + webpackBundlingMode := BundlingMode.LibraryAndApplication(), // BundlingMode.Application, + Compile / scalaJSModuleInitializers += { + org.scalajs.linker.interface.ModuleInitializer.mainMethod("fmgp.webapp.App", "main") + }, + ) + // ############################ // #### Release process ##### // ############################ diff --git a/mediator/src/main/resources/public/atala-prism-logo-suite.svg b/mediator/src/main/resources/public/atala-prism-logo-suite.svg new file mode 100644 index 00000000..52e4aece --- /dev/null +++ b/mediator/src/main/resources/public/atala-prism-logo-suite.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mediator/src/main/resources/public/index.html b/mediator/src/main/resources/public/index.html new file mode 100644 index 00000000..108dfffe --- /dev/null +++ b/mediator/src/main/resources/public/index.html @@ -0,0 +1,33 @@ + + + + + IOHK Mediator + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorAgent.scala b/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorAgent.scala index bddf1e93..9204d3ee 100644 --- a/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorAgent.scala +++ b/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorAgent.scala @@ -22,6 +22,7 @@ import zio.json.* import scala.concurrent.ExecutionContext.Implicits.global import scala.util.Try +import scala.io.Source case class MediatorAgent( override val id: DID, override val keyStore: KeyStore, // Should we make it lazy with ZIO @@ -114,39 +115,39 @@ case class MediatorAgent( MediatorError, Option[EncryptedMessage] ] = - ZIO - .logAnnotate("msgHash", msg.hashCode.toString) { - for { - _ <- ZIO.log("receivedMessage") - maybeSyncReplyMsg <- - if (!msg.recipientsSubject.contains(id)) - ZIO.logError(s"This mediator '${id.string}' is not a recipient") - *> ZIO.none - else - for { - messageItemRepo <- ZIO.service[MessageItemRepo] - _ <- messageItemRepo.insert(MessageItem(msg)) // store all message - plaintextMessage <- decrypt(msg) - _ <- didSocketManager.get.flatMap { m => // TODO HACK REMOVE !!!!!!!!!!!!!!!!!!!!!!!! - ZIO.foreach(m.tapSockets)(_.socketOutHub.publish(TapMessage(msg, plaintextMessage).toJson)) - } - _ <- mSocketID match + ZIO + .logAnnotate("msgHash", msg.hashCode.toString) { + for { + _ <- ZIO.log("receivedMessage") + maybeSyncReplyMsg <- + if (!msg.recipientsSubject.contains(id)) + ZIO.logError(s"This mediator '${id.string}' is not a recipient") + *> ZIO.none + else + for { + messageItemRepo <- ZIO.service[MessageItemRepo] + _ <- messageItemRepo.insert(MessageItem(msg)) // store all message + plaintextMessage <- decrypt(msg) + _ <- didSocketManager.get.flatMap { m => // TODO HACK REMOVE !!!!!!!!!!!!!!!!!!!!!!!! + ZIO.foreach(m.tapSockets)(_.socketOutHub.publish(TapMessage(msg, plaintextMessage).toJson)) + } + _ <- mSocketID match + case None => ZIO.unit + case Some(socketID) => + plaintextMessage.from match case None => ZIO.unit - case Some(socketID) => - plaintextMessage.from match - case None => ZIO.unit - case Some(from) => - didSocketManager.update { - _.link(from.asFROMTO, socketID) - } - // TODO Store context of the decrypt unwarping - // TODO SreceiveMessagetore context with MsgID and PIURI - protocolHandler <- ZIO.service[ProtocolExecuter[Services]] - ret <- protocolHandler - .execute(plaintextMessage) - .tapError(ex => ZIO.logError(s"Error when execute Protocol: $ex")) - } yield ret - } yield maybeSyncReplyMsg + case Some(from) => + didSocketManager.update { + _.link(from.asFROMTO, socketID) + } + // TODO Store context of the decrypt unwarping + // TODO SreceiveMessagetore context with MsgID and PIURI + protocolHandler <- ZIO.service[ProtocolExecuter[Services]] + ret <- protocolHandler + .execute(plaintextMessage) + .tapError(ex => ZIO.logError(s"Error when execute Protocol: $ex")) + } yield ret + } yield maybeSyncReplyMsg } .provideSomeLayer( /*resolverLayer ++ indentityLayer ++*/ protocolHandlerLayer) @@ -259,12 +260,28 @@ object MediatorAgent { .text(s"The content-type must be ${MediaTypes.SIGNED.typ} or ${MediaTypes.ENCRYPTED.typ}") .setStatus(Status.BadRequest) ) + case req @ Method.GET -> !! => { // html.Html.fromDomElement() + val data = Source.fromResource(s"public/index.html").mkString("") + ZIO.log("index.html") *> ZIO.succeed(Response.html(data)) + } }: Http[ Operations & Resolver & MessageDispatcher & MediatorAgent & MessageItemRepo & UserAccountRepo, Throwable, Request, Response ] + } ++ Http.fromResource(s"public/webapp-fastopt-library.js").when { + case Method.GET -> !! / "public" / "webapp-fastopt-library.js" => true + case _ => false + } ++ { + Http.fromResource(s"public/webapp-fastopt-bundle.js").when { + case Method.GET -> !! / "public" / path => true + // Response( + // body = Body.fromStream(ZStream.fromIterator(Source.fromResource(s"public/$path").iter).map(_.toByte)), + // headers = Headers(HeaderNames.contentType, HeaderValues.applicationJson), + // ) + case _ => false + } } @@ HttpAppMiddleware.cors( zio.http.middleware.Cors.CorsConfig( @@ -272,7 +289,7 @@ object MediatorAgent { allowedMethods = Some(Set(Method.GET, Method.POST, Method.OPTIONS)), ) ) - @@ + @@ HttpAppMiddleware.updateHeaders(headers => Headers( headers.map(h => @@ -281,5 +298,5 @@ object MediatorAgent { } else h ) ) - ) + ) } diff --git a/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorStandalone.scala b/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorStandalone.scala index 7a0be55c..85dd5198 100644 --- a/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorStandalone.scala +++ b/mediator/src/main/scala/io/iohk/atala/mediator/app/MediatorStandalone.scala @@ -126,6 +126,7 @@ object MediatorStandalone extends ZIOAppDefault { .fork _ <- ZIO.log(s"Mediator Started") _ <- myServer.join *> ZIO.log(s"Mediator End") + _ <- ZIO.log(s"*" * 100) } yield () } diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 00000000..d13af100 --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,28 @@ +# WEBAPP module + +## Compile + +Run sbt with the following NODE_OPTIONS. + +This will prevent `Error: error:0308010C:digital envelope routines::unsupported`. See [Troubleshooting (Node v17)](../README.md#Troubleshooting) + +```shell +NODE_OPTIONS=--openssl-legacy-provider sbt +``` + +## build and run app (open chrome) + +`sbt>` `webapp / Compile / fastOptJS / webpack` + +open `file:///home/fabio/workspace/ScalaDID/webapp/index-fastopt.html#/` + +google-chrome-stable --disable-web-security --user-data-dir="/tmp/chrome_tmp" --new-window file:///home/fabio/workspace/ScalaDID/webapp/index-fastopt.html#/ + +When developing in a recompile-test iteration you can sbt to monitor source files and re-run. +```sbt +~ webapp / Compile / fastOptJS / webpack +``` + +## TODO LIST: + +- Update to the [material-web version 3](https://github.com/material-components/material-web#readme). diff --git a/webapp/index-fastopt.html b/webapp/index-fastopt.html new file mode 100644 index 00000000..de05ff99 --- /dev/null +++ b/webapp/index-fastopt.html @@ -0,0 +1,34 @@ + + + + + + [FAST] fmgp ipfs webapp + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/webapp/index-fullopt.html b/webapp/index-fullopt.html new file mode 100644 index 00000000..1929c213 --- /dev/null +++ b/webapp/index-fullopt.html @@ -0,0 +1,18 @@ + + + + + + [FULL] fmgp ipfs webapp + + + + + + + + +
+ + + \ No newline at end of file diff --git a/webapp/src/main/scala/fmgp/webapp/App.scala b/webapp/src/main/scala/fmgp/webapp/App.scala new file mode 100644 index 00000000..afbd0456 --- /dev/null +++ b/webapp/src/main/scala/fmgp/webapp/App.scala @@ -0,0 +1,49 @@ +package fmgp.webapp + +import scala.scalajs.js.annotation._ + +import scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L._ +import com.raquo.waypoint._ + +import MyRouter._ +import com.raquo.airstream.ownership.ManualOwner + +import org.scalajs.dom.ServiceWorkerRegistration +import scala.scalajs.js.JSON +object App { + + def main( /*args: Array[String]*/ ): Unit = { + + // This div, its id and contents are defined in index-fastopt.html and index-fullopt.html files + lazy val container = dom.document.getElementById("app-container") + + lazy val appElement = { + div( + AppUtils.drawer(linkPages, MyRouter.router.currentPageSignal), + AppUtils.drawerScrim, + AppUtils.topBarHeader(MyRouter.router.currentPageSignal.map { case p: MediatorPage.type => + "IOHK DID Comm Mediator" + // case p => p.title + }), + mainTag( + className("mdc-top-app-bar--fixed-adjust"), + child <-- $selectedApp.signal + ) + ) + } + + // Wait until the DOM is loaded, otherwise app-container element might not exist + renderOnDomContentLoaded(container, appElement) + + } + + private val $selectedApp = SplitRender(MyRouter.router.currentPageSignal) + .collectStatic(MediatorPage)(MediatorInfo()) + + private val linkPages: List[Page] = List( + MediatorPage, + ) + +} diff --git a/webapp/src/main/scala/fmgp/webapp/AppUtils.scala b/webapp/src/main/scala/fmgp/webapp/AppUtils.scala new file mode 100644 index 00000000..41bc4d21 --- /dev/null +++ b/webapp/src/main/scala/fmgp/webapp/AppUtils.scala @@ -0,0 +1,167 @@ +package fmgp.webapp + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport + +import org.scalajs.dom +import com.raquo.laminar.codecs._ +import com.raquo.laminar.api.L._ + +import MyRouter._ + +@JSExportTopLevel("AppUtils") +object AppUtils { + + def onEnterPress = onKeyPress.filter(_.keyCode == dom.ext.KeyCode.Enter) + + val menuClickObserver = Observer[dom.MouseEvent](onNext = ev => { + import typings.materialDrawer.mod.MDCDrawer + val tmp = MDCDrawer.attachTo(dom.window.document.querySelector(".mdc-drawer")) + tmp.open_=(!tmp.open) + }) + + val optionsClickObserver = Observer[dom.MouseEvent](onNext = ev => { + import typings.materialMenu.mod.MDCMenu + val tmp = MDCMenu.attachTo(dom.window.document.querySelector(".mdc-menu")) + tmp.open_=(!tmp.open) + }) + + def topBarHeader(title: Signal[String]) = { // (title: String) = { + val menuButton = button( + className("material-icons mdc-top-app-bar__navigation-icon mdc-icon-button"), + aria.label("Options"), + onClick --> menuClickObserver, + "menu" + ) + typings.materialRipple.mod.MDCRipple.attachTo(menuButton.ref) + + // def makeLi(didName: String, icon: String) = + // li( + // className("mdc-list-item"), + // role("menuitem"), + // span(className("mdc-list-item__ripple")), + // i(className("material-icons mdc-list-item__graphic"), icon), // FIXME icon make a make a clone of icon + // // span( + // // className("mdc-list-item__graphic mdc-menu__selection-group-icon"), + // // i(aria.label("Atomium"), atomiumSVG) + // // ), + // span(className("mdc-list-item__text"), didName), + // onClick --> Observer[org.scalajs.dom.MouseEvent](onNext = + // ev => Global.agentVar.update(e => fmgp.did.AgentProvider.allAgents.get(didName)), + // ) + // ) + + val options = { + div( + className("mdc-menu mdc-menu-surface"), + minWidth("200px"), + ul( + className("mdc-list"), + role("menu"), + aria.hidden(true), + aria.orientation("vertical"), + tabIndex(-1), + li(className("mdc-list-divider"), role("separator")), + li( + ul( + className("mdc-menu__selection-group"), + // Global.dids.map { did => makeLi(did, "person_outline") } + ) + ), + ) + ) + } + + val optionsButton = button( + className("material-icons mdc-top-app-bar__navigation-icon mdc-icon-button"), + aria.label("Open navigation menu"), + onClick --> optionsClickObserver, + "more_vert" + ) + typings.materialMenu.mod.MDCMenu.attachTo(options.ref) + + headerTag( + className("mdc-top-app-bar"), + div( + className("mdc-top-app-bar__row"), + sectionTag( + className("mdc-top-app-bar__section mdc-top-app-bar__section--align-start"), + menuButton, + span(className("mdc-top-app-bar__title"), child.text <-- title) + ), + sectionTag( + className("mdc-top-app-bar__section mdc-top-app-bar__section--align-end"), + role("toolbar"), + a( + className("material-icons mdc-top-app-bar__action-item mdc-icon-button"), + href("https://github.com/FabioPinheiro/fmgp-generative-design"), + i(aria.label("Github")), + ), + // button( + // className("material-icons mdc-top-app-bar__action-item mdc-icon-button"), + // aria.label("Search"), + // "search" + // ), + // select( + // value <-- Global.agentVar.signal.map(Global.getAgentName(_)), + // onChange.mapToValue.map(e => fmgp.did.AgentProvider.allAgents.get(e)) --> Global.agentVar, + // Global.dids.map { step => option(value := step, step) } + // ), + div( + className("mdc-menu-surface--anchor"), + optionsButton, + options + ) + ), + ), + ) + } + + val drawerScrim = div(className("mdc-drawer-scrim")) + def drawer(linkPages: List[Page], currentPage: Signal[Page]) = + asideTag( + className("mdc-drawer mdc-drawer--modal"), + div( + className("mdc-drawer__header"), + h3(className("mdc-drawer__title"), "IOHP - Atala PRISM Mediator"), + h6(className("mdc-drawer__subtitle"), "atlaprism@iohk.io"), + ), + div( + className("mdc-drawer__content"), + navTag( + className("mdc-list"), + linkPages.map(page => + a( + className <-- currentPage.map { p => + if (p == page) "mdc-list-item mdc-list-item--activated" else "mdc-list-item" + }, + aria.current := "page", + tabIndex(0), + span(className("mdc-list-item__ripple")), + i( + className("material-icons mdc-list-item__graphic"), + aria.hidden(true), + page.icon + ), + navigateTo(page), + span(className("mdc-list-item__text"), page.title), + ), + ) + ) + ) + ) + + def myButton(text: String) = { + div( + className("mdc-touch-target-wrapper"), + button( + className("mdc-button mdc-button--touch mdc-button--raised"), + span(className("mdc-button__ripple")), + span(className("mdc-button__touch")), + span(className("mdc-button__label"), text), + ) + ) + + } + +} diff --git a/webapp/src/main/scala/fmgp/webapp/Global.scala b/webapp/src/main/scala/fmgp/webapp/Global.scala new file mode 100644 index 00000000..1ecc97b2 --- /dev/null +++ b/webapp/src/main/scala/fmgp/webapp/Global.scala @@ -0,0 +1,22 @@ +package fmgp.webapp + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSExport +import org.scalajs.dom +import com.raquo.laminar.api.L._ + +import fmgp.did._ +import fmgp.did.method.peer.DIDPeer +import fmgp.did.comm.TO + +import fmgp.did.comm._ + +object Global { + + def mediatorDID = FROM( + "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjoiaHR0cHM6Ly9rOHMtaW50LmF0YWxhcHJpc20uaW8vbWVkaWF0b3IiLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19" + ) + def clipboardSideEffect(text: => String): Any => Unit = + (_: Any) => { dom.window.navigator.clipboard.writeText(text) } + +} diff --git a/webapp/src/main/scala/fmgp/webapp/MediatorInfo.scala b/webapp/src/main/scala/fmgp/webapp/MediatorInfo.scala new file mode 100644 index 00000000..f8b67aab --- /dev/null +++ b/webapp/src/main/scala/fmgp/webapp/MediatorInfo.scala @@ -0,0 +1,41 @@ +package fmgp.webapp + +import org.scalajs.dom +import com.raquo.laminar.api.L._ +import typings.qrcodeGenerator + +import zio.json._ +import fmgp.did._ +import fmgp.did.comm._ +import fmgp.did.comm.protocol.oobinvitation.OOBInvitation + +object MediatorInfo { + + val invitation = OOBInvitation( + from = Global.mediatorDID, + goal_code = Some("request-mediate"), + goal = Some("RequestMediate"), + accept = Some(Seq("didcomm/v2")), + ) + val qrCodeData = OutOfBandPlaintext.from(invitation.toPlaintextMessage).makeURI("https://did.fmgp.app/#/") + + val divQRCode = div() + { + val aux = qrcodeGenerator.mod.^.apply(qrcodeGenerator.TypeNumber.`0`, qrcodeGenerator.ErrorCorrectionLevel.L) + aux.addData(qrCodeData) + aux.make() + divQRCode.ref.innerHTML = aux.createSvgTag(8d) + } + + def apply(): HtmlElement = // rootElement + div( + h1("Invite for the DID Comm Mediator:"), + h3("Plaintext out of band invitation:"), + p(code(qrCodeData)), + pre(code(invitation.toPlaintextMessage.toJsonPretty)), + divQRCode, + h3("Signed out of band invitation:"), + code("TODO"), + ) + +} diff --git a/webapp/src/main/scala/fmgp/webapp/MyRouter.scala b/webapp/src/main/scala/fmgp/webapp/MyRouter.scala new file mode 100644 index 00000000..29094868 --- /dev/null +++ b/webapp/src/main/scala/fmgp/webapp/MyRouter.scala @@ -0,0 +1,104 @@ +package fmgp.webapp + +import com.raquo.laminar.api.L.{_, given} +import com.raquo.waypoint._ +import org.scalajs.dom +import upickle.default._ + +object MyRouter { + sealed abstract class Page( + val title: String, + val icon: String // https://fonts.google.com/icons?selected=Material+Icons+Outlined + ) + + // case object HomePage extends Page("Home", "home") + // case class OOBPage(query_oob: String) extends Page("OutOfBand", "app_shortcut") + // case object DocPage extends Page("Doc", "menu_book") + // case object AgentKeysPage extends Page("AgentKeys", "key") + // // case object DIDPage extends Page("DID", "visibility") + // case object AgentDBPage extends Page("MessageDB", "folder_open") + // case class ResolverPage(did: String) extends Page("Resolver", "dns") + // case object EncryptPage extends Page("Encrypt", "enhanced_encryption") + // case object DecryptPage extends Page("Decrypt", "email") + // case object BasicMessagePage extends Page("BasicMessage", "message") + // case object TrustPingPage extends Page("TrustPing", "network_ping`") + // case object TapIntoStreamPage extends Page("TapIntoStream", "chat") + // case object DAppStorePage extends Page("DAppStore", "share") + case object MediatorPage extends Page("Mediator", "diversity_3") + + // given homePageRW: ReadWriter[HomePage.type] = macroRW + // given oobPageRW: ReadWriter[OOBPage] = macroRW + // given docPageRW: ReadWriter[DocPage.type] = macroRW + // given keysPageRW: ReadWriter[AgentKeysPage.type] = macroRW + // given agentDBPageRW: ReadWriter[AgentDBPage.type] = macroRW + // given resolverPageRW: ReadWriter[ResolverPage] = macroRW + // given encryptPageRW: ReadWriter[EncryptPage.type] = macroRW + // given decryptPageRW: ReadWriter[DecryptPage.type] = macroRW + // given basicMessagePageRW: ReadWriter[BasicMessagePage.type] = macroRW + // given trustPingPageRW: ReadWriter[TrustPingPage.type] = macroRW + // given tapIntoStreamPageRW: ReadWriter[TapIntoStreamPage.type] = macroRW + // given dAppStorePageRW: ReadWriter[DAppStorePage.type] = macroRW + given mediatorPageRW: ReadWriter[MediatorPage.type] = macroRW + + given rw: ReadWriter[Page] = macroRW + + private val routes = List( + // // http://localhost:8080/?_oob=eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNTk5ZjM2MzgtYjU2My00OTM3LTk0ODctZGZlNTUwOTlkOTAwIiwiZnJvbSI6ImRpZDpleGFtcGxlOnZlcmlmaWVyIiwiYm9keSI6eyJnb2FsX2NvZGUiOiJzdHJlYW1saW5lZC12cCIsImFjY2VwdCI6WyJkaWRjb21tL3YyIl19fQ + // Route.onlyQuery[OOBPage, String]( // OOB + // encode = page => page.query_oob, + // decode = arg => OOBPage(query_oob = arg), + // pattern = (root / endOfSegments) ? (param[String]("_oob")), + // Router.localFragmentBasePath + // ), + // Route[ResolverPage, String]( + // encode = page => page.did, + // decode = arg => ResolverPage(did = arg), + // pattern = root / "resolver" / segment[String] / endOfSegments, + // Router.localFragmentBasePath + // ), + // Route.static(HomePage, root / endOfSegments, Router.localFragmentBasePath), + // Route.static(DocPage, root / "doc" / endOfSegments, Router.localFragmentBasePath), + // Route.static(AgentKeysPage, root / "agentkeys" / endOfSegments, Router.localFragmentBasePath), + // Route.static(AgentDBPage, root / "db" / endOfSegments, Router.localFragmentBasePath), + // Route.static(EncryptPage, root / "encrypt" / endOfSegments, Router.localFragmentBasePath), + // Route.static(DecryptPage, root / "decrypt" / endOfSegments, Router.localFragmentBasePath), + // Route.static(BasicMessagePage, root / "basicmessage" / endOfSegments, Router.localFragmentBasePath), + // Route.static(TrustPingPage, root / "trustping" / endOfSegments, Router.localFragmentBasePath), + // Route.static(TapIntoStreamPage, root / "stream" / endOfSegments, Router.localFragmentBasePath), + // Route.static(DAppStorePage, root / "dapp" / endOfSegments, Router.localFragmentBasePath), + // Route.static(MediatorPage, root / "mediator" / endOfSegments, Router.localFragmentBasePath), + Route.static(MediatorPage, root / endOfSegments, Router.localFragmentBasePath), + ) + + val router = new Router[Page]( + routes = routes, + getPageTitle = _.title, // displayed in the browser tab next to favicon + serializePage = page => write(page)(rw), // serialize page data for storage in History API log + deserializePage = pageStr => read(pageStr)(rw), // deserialize the above + // routeFallback = { (_: String) => HomePage }, + routeFallback = { (_: String) => MediatorPage }, + )( + popStateEvents = windowEvents(_.onPopState), // this is how Waypoint avoids an explicit dependency on Laminar + owner = unsafeWindowOwner // this router will live as long as the window + ) + + // Note: for fragment ('#') URLs this isn't actually needed. + // See https://github.com/raquo/Waypoint docs for why this modifier is useful in general. + def navigateTo(page: Page): Binder[HtmlElement] = Binder { el => + + val isLinkElement = el.ref.isInstanceOf[dom.html.Anchor] + + if (isLinkElement) { + el.amend(href(router.absoluteUrlForPage(page))) + } + + // If element is a link and user is holding a modifier while clicking: + // - Do nothing, browser will open the URL in new tab / window / etc. depending on the modifier key + // Otherwise: + // - Perform regular pushState transition + (onClick + .filter(ev => !(isLinkElement && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey))) + .preventDefault + --> (_ => router.pushState(page))).bind(el) + } +}