diff --git a/.gitignore b/.gitignore index 9d44b0e2..b57ed150 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ npm-debug.log /assets/node_modules/ .env +.DS_Store +xcuserdata diff --git a/config/config.exs b/config/config.exs index 2327c3c6..b2f0e39e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -52,12 +52,21 @@ config :logger, :console, # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason +config :phoenix, :template_engines, neex: LiveViewNative.Engine +config :phoenix_template, :format_encoders, swiftui: Phoenix.HTML.Engine config :ash, :use_all_identities_in_manage_relationship?, false config :tesla, :adapter, {Tesla.Adapter.Finch, name: Spendable.Finch} config :oauth2, adapter: {Tesla.Adapter.Finch, name: Spendable.Finch} +config :spendable, Spendable.Guardian, + issuer: "https://accounts.google.com", + verify_issuer: true, + ttl: {1, :hour}, + allowed_algos: ["RS256"], + secret_fetcher: Spendable.Guardian.KeyServer + config :goth, project_id: "cloud-57" config :ueberauth, Ueberauth, @@ -65,6 +74,25 @@ config :ueberauth, Ueberauth, google: {Ueberauth.Strategy.Google, []} ] +config :mime, :types, %{ + "text/swiftui" => ["swiftui"], + "text/styles" => ["styles"] +} + +config :live_view_native, + plugins: [ + LiveViewNative.HTML, + LiveViewNative.SwiftUI + ] + +config :live_view_native_stylesheet, + content: [ + swiftui: [ + "lib/**/*swiftui*" + ] + ], + output: "priv/static/assets" + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index 8adf80ae..6974e147 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -35,7 +35,7 @@ config :spendable, SpendableWeb.Endpoint, patterns: [ ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", - ~r"lib/spendable_web/(controllers|live|components)/.*(ex|heex)$" + ~r"lib/spendable_web/(controllers|live|components|styles)/.*(ex|heex,neex)$" ] ] @@ -53,3 +53,7 @@ config :phoenix, :stacktrace_depth, 20 config :phoenix, :plug_init_mode, :runtime config :spendable, Plaid, base_url: "https://sandbox.plaid.com" + +config :live_view_native_stylesheet, + annotations: true, + pretty: true diff --git a/lib/spendable/application.ex b/lib/spendable/application.ex index 16396b97..f3ff03cd 100644 --- a/lib/spendable/application.ex +++ b/lib/spendable/application.ex @@ -9,6 +9,7 @@ defmodule Spendable.Application do def start(_type, _args) do all_env_children = [ {Finch, name: Spendable.Finch}, + Spendable.Guardian.KeyServer, SpendableWeb.Telemetry, Spendable.Repo, {Phoenix.PubSub, name: Spendable.PubSub}, diff --git a/lib/spendable/guardian.ex b/lib/spendable/guardian.ex new file mode 100644 index 00000000..204b7586 --- /dev/null +++ b/lib/spendable/guardian.ex @@ -0,0 +1,11 @@ +defmodule Spendable.Guardian do + use Guardian, otp_app: :spendable + + def subject_for_token(_sub, _claims) do + {:error, :not_supported} + end + + def resource_from_claims(_claims) do + {:error, :not_supported} + end +end diff --git a/lib/spendable/guardian/key_server.ex b/lib/spendable/guardian/key_server.ex new file mode 100644 index 00000000..ec6d8255 --- /dev/null +++ b/lib/spendable/guardian/key_server.ex @@ -0,0 +1,47 @@ +defmodule Spendable.Guardian.KeyServer do + @behaviour Guardian.Token.Jwt.SecretFetcher + + use GenServer + + def start_link(_opts) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl GenServer + def init(_opts) do + :ets.new(__MODULE__, [:named_table, :public]) + {:ok, nil} + end + + @impl Guardian.Token.Jwt.SecretFetcher + # coveralls-ignore-next-line not supported + def fetch_signing_secret(_mod, _opts), do: {:error, :not_supported} + + @impl Guardian.Token.Jwt.SecretFetcher + def fetch_verifying_secret(_mod, %{"kid" => kid}, _opts) do + case lookup(kid) do + {:ok, jwk} -> + {:ok, jwk} + + :error -> + load_public_keys() + lookup(kid) + end + end + + defp lookup(kid) do + case :ets.lookup(__MODULE__, kid) do + [{^kid, key}] -> {:ok, key} + [] -> :error + end + end + + defp load_public_keys() do + {:ok, %{body: body}} = Tesla.get("https://www.googleapis.com/oauth2/v3/certs") + %{"keys" => public_keys} = Jason.decode!(body) + + Enum.each(public_keys, fn %{"kid" => kid} = key -> + :ets.insert(__MODULE__, {kid, JOSE.JWK.from(key)}) + end) + end +end diff --git a/lib/spendable_native.ex b/lib/spendable_native.ex new file mode 100644 index 00000000..828db6c9 --- /dev/null +++ b/lib/spendable_native.ex @@ -0,0 +1,72 @@ +defmodule SpendableNative do + import SpendableWeb, only: [verified_routes: 0] + + def live_view() do + quote do + use LiveViewNative.LiveView, + formats: [ + :swiftui + ], + layouts: [ + swiftui: {SpendableWeb.Layouts.SwiftUI, :app} + ] + + unquote(verified_routes()) + end + end + + def render_component(opts) do + opts = + opts + |> Keyword.take([:format]) + |> Keyword.put(:as, :render) + + quote do + use LiveViewNative.Component, unquote(opts) + + unquote(helpers(opts[:format])) + end + end + + def component(opts) do + opts = Keyword.take(opts, [:format, :root, :as]) + + quote do + use LiveViewNative.Component, unquote(opts) + + unquote(helpers(opts[:format])) + end + end + + def layout(opts) do + opts = Keyword.take(opts, [:format, :root]) + + quote do + use LiveViewNative.Component, unquote(opts) + + import LiveViewNative.Component, only: [csrf_token: 1] + + unquote(helpers(opts[:format])) + end + end + + defp helpers(_format) do + gettext_quoted = + quote do + import SpendableWeb.Gettext + end + + [gettext_quoted, verified_routes()] + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__([which | opts]) when is_atom(which) do + apply(__MODULE__, which, [opts]) + end + + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/spendable_web.ex b/lib/spendable_web.ex index d1a97a21..40808ddc 100644 --- a/lib/spendable_web.ex +++ b/lib/spendable_web.ex @@ -54,6 +54,12 @@ defmodule SpendableWeb do use Phoenix.LiveView, layout: {SpendableWeb.Layouts, :app} + use LiveViewNative.LiveView, + formats: [:swiftui], + layouts: [ + swiftui: {SpendableWeb.Layouts.SwiftUI, :app} + ] + unquote(html_helpers()) end end diff --git a/lib/spendable_web/components/layouts.swiftui.ex b/lib/spendable_web/components/layouts.swiftui.ex new file mode 100644 index 00000000..66a6d07b --- /dev/null +++ b/lib/spendable_web/components/layouts.swiftui.ex @@ -0,0 +1,5 @@ +defmodule SpendableWeb.Layouts.SwiftUI do + use SpendableNative, [:layout, format: :swiftui] + + embed_templates "layouts_swiftui/*" +end diff --git a/lib/spendable_web/components/layouts_swiftui/app.swiftui.heex b/lib/spendable_web/components/layouts_swiftui/app.swiftui.heex new file mode 100644 index 00000000..f3985a1e --- /dev/null +++ b/lib/spendable_web/components/layouts_swiftui/app.swiftui.heex @@ -0,0 +1 @@ +<%= @inner_content %> \ No newline at end of file diff --git a/lib/spendable_web/components/layouts_swiftui/root.swiftui.heex b/lib/spendable_web/components/layouts_swiftui/root.swiftui.heex new file mode 100644 index 00000000..3ef2df95 --- /dev/null +++ b/lib/spendable_web/components/layouts_swiftui/root.swiftui.heex @@ -0,0 +1,5 @@ +<.csrf_token /> +