npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@geekapps/auth-react-native

v0.9.2

Published

Geekapps Auth SDK for React Native (Expo and bare) apps — PKCE login, device flow, token refresh.

Readme

@geekapps/auth-react-native

SDK de login para apps React Native (Expo ou bare) que usam o Geekapps Auth (app-api) como Authorization Server.

Cobre:

  • Login via Authorization Code + PKCE, abrindo o navegador do sistema (Linking.openURL por padrão, ou um browserOpener customizado — ex: expo-web-browser);
  • Troca de code por tokens, com renovação automática (refresh_token) quando o access token está perto de expirar;
  • Device Authorization Grant (útil pra fluxos de segundo dispositivo);
  • Storage de tokens plugável — a lib não força nenhuma dependência nativa; em produção, injete um adapter sobre expo-secure-store ou react-native-keychain;
  • request() — fetch autenticado contra o app-api, injeta o Bearer token e renova sozinho se necessário;
  • Gerenciamento de organizações — organizações, membros, convites, roles, visibilidade pública/privada e solicitações de entrada (useOrg(), useMembers(), useRoles(), usePublicOrganizations(), useJoinRequests()).

Núcleo (core.ts) é JS puro, sem imports de react-native — só client.tsx depende de Linking. crypto.getRandomValues precisa estar disponível no runtime (ver "Instalação").

Instalação

bun add @geekapps/auth-react-native
# polyfill de crypto.getRandomValues, necessário pro PKCE
bun add react-native-get-random-values

No topo do index.js/App.tsx (antes de qualquer outro import):

import "react-native-get-random-values";

Expo SDK 53+: adicione ao .env do app:

EXPO_PUBLIC_USE_RN_FETCH=1

A partir do SDK 53 o Expo substitui o fetch global pelo runtime "Winter" (WinterCG), que só aceita Blob/File de verdade em FormData — não o formato clássico do React Native ({ uri, type, name }) que uploadAvatar() usa. Sem essa env var, o upload de avatar falha com Unsupported FormDataPart implementation antes mesmo de qualquer chamada de rede. EXPO_PUBLIC_USE_RN_FETCH=1 reverte o app para o fetch clássico do React Native.

Uso

import { GeekappsAuthProvider, useAuth } from "@geekapps/auth-react-native";

export function App() {
  return (
    <GeekappsAuthProvider
      config={{
        issuer: "https://auth.suaapp.com",
        clientId: "gk_client_xxx",
        redirectUri: "myapp://callback", // precisa de pkceFlowEnabled=true na Application
        scopes: ["openid", "profile", "email"],
      }}
    >
      <Home />
    </GeekappsAuthProvider>
  );
}

function Home() {
  const { isAuthenticated, isLoading, user, signIn, signOut, request } = useAuth();

  if (isLoading) return null;

  if (!isAuthenticated) {
    return <Button title="Entrar" onPress={() => signIn()} />;
  }

  return (
    <View>
      <Text>Olá, {user?.name as string}</Text>
      <Button title="Sair" onPress={signOut} />
    </View>
  );
}

Storage seguro (recomendado em produção)

import * as SecureStore from "expo-secure-store";
import type { TokenStorage } from "@geekapps/auth-react-native";

const secureStorage: TokenStorage = {
  getItem: (key) => SecureStore.getItemAsync(key),
  setItem: (key, value) => SecureStore.setItemAsync(key, value),
  removeItem: (key) => SecureStore.deleteItemAsync(key),
};

<GeekappsAuthProvider config={config} storage={secureStorage}>

Browser nativo em apps Expo

Por padrão o login abre no navegador externo (Linking.openURL). Em apps Expo, expo-web-browser dá uma UX melhor (Custom Tabs/ASWebAuthenticationSession, sem sair do app):

import * as WebBrowser from "expo-web-browser";

<GeekappsAuthProvider
  config={config}
  browserOpener={(url) => WebBrowser.openBrowserAsync(url).then(() => undefined)}
>

Device flow (segundo dispositivo)

const { startDeviceFlow, waitForDeviceAuthorization } = useAuth();

const device = await startDeviceFlow();
// mostra device.userCode e device.verificationUri pro usuário
await waitForDeviceAuthorization(device.deviceCode, device.interval);

Upload de avatar

import * as ImagePicker from "expo-image-picker";

const { uploadAvatar } = useAuth();

async function pickAndUploadAvatar() {
  const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: "images" });
  if (result.canceled) return;
  const asset = result.assets[0];

  // Um único POST multipart/form-data pro app-api, que armazena o arquivo
  // e recarrega o user — imageUrl/picture já vem assinada quando necessário
  // (é o app-api quem decide/assina; este SDK nunca fala com o storage).
  const updatedUser = await uploadAvatar({
    uri: asset.uri,
    name: asset.fileName ?? "avatar.jpg",
    type: asset.mimeType ?? "image/jpeg",
    size: asset.fileSize,
  });

  console.log(updatedUser?.picture);
}

Requer EXPO_PUBLIC_USE_RN_FETCH=1 em apps Expo SDK 53+ — ver "Instalação" acima. Sem isso, falha com Unsupported FormDataPart implementation.

Organizações

O usuário logado pode pertencer a várias organizações (Organization). O provider já carrega a lista automaticamente após o login (organizations, currentOrg, currentOrgId, isLoadingOrganizations) — não precisa buscar manualmente.

import { useOrg } from "@geekapps/auth-react-native";

function OrgSwitcher() {
  const { organizations, currentOrg, setCurrentOrgId, isLoadingOrganizations } = useOrg();

  if (isLoadingOrganizations) return null;

  return (
    <View>
      <Text>Organização atual: {currentOrg?.name ?? "nenhuma"}</Text>
      {organizations.map((org) => (
        <Button key={org.id} title={org.name} onPress={() => setCurrentOrgId(org.id)} />
      ))}
    </View>
  );
}

currentOrgId é só um seletor local (qual organização a UI está exibindo) — trocar não faz nenhuma chamada de rede, e não afeta os tokens/sessão atuais.

CRUD de organizações

const { createOrganization, updateOrganization, deleteOrganization } = useOrg();

// applicationId é preenchido automaticamente com o clientId da config — não precisa passar.
const org = await createOrganization({
  name: "Acme Inc",
  slug: "acme-inc",
  description: "Minha empresa",
  imageUrl: "https://...",
});

await updateOrganization(org.id, { name: "Acme Inc.", status: "active" });

// Soft delete (marca status: "inactive") — só o dono da organização pode.
await deleteOrganization(org.id);

Organizações públicas x privadas

Toda organização nasce privada (visibility: "private") — só aparece pra quem já é membro/dono, e a única forma de entrar é convite ou adição direta por um admin. Marcando visibility: "public" (na criação ou depois via updateOrganization), ela passa a aparecer na listagem pública do app e qualquer usuário autenticado pode solicitar entrada, sem precisar de convite.

// Cria já como pública
const org = await createOrganization({ name: "Comunidade Acme", slug: "comunidade-acme", visibility: "public" });

// Ou torna pública uma organização que já existia como privada
await updateOrganization(org.id, { visibility: "public" });

// Volta a ser privada — some da listagem pública, mas quem já é membro continua sendo
await updateOrganization(org.id, { visibility: "private" });

Esse é o gancho pra apps que fazem sentido com "comunidades" ou "times abertos" que qualquer usuário pode pedir pra entrar (ex: um app de bairro, uma liga de jogo, um espaço de coworking) — sem precisar gerenciar convite por convite.

Descobrindo organizações públicas

usePublicOrganizations() é totalmente separado de useOrg(): useOrg().organizations são as organizações do usuário logado (as que ele já é membro/dono); listPublicOrganizations() é a vitrine pública do app inteiro — organizações visibility: "public" e ativas, visível a qualquer usuário autenticado, sem exigir membership. Organizações privadas nunca aparecem aqui.

import { usePublicOrganizations } from "@geekapps/auth-react-native";

function DiscoverOrganizations() {
  const { listPublicOrganizations } = usePublicOrganizations();
  const [orgs, setOrgs] = useState<PublicOrganization[]>([]);

  useEffect(() => {
    listPublicOrganizations().then(setOrgs);
  }, [listPublicOrganizations]);

  return (
    <FlatList
      data={orgs}
      keyExtractor={(org) => org.id}
      renderItem={({ item }) => <Text>{item.name}</Text>}
    />
  );
}

Solicitar entrada numa organização pública (join request)

import { useJoinRequests } from "@geekapps/auth-react-native";

const {
  requestToJoin,
  getMyJoinRequest,
  cancelJoinRequest,
  listJoinRequests,
  approveJoinRequest,
  rejectJoinRequest,
} = useJoinRequests();

// Usuário pede pra entrar (falha com 403 se a org for privada)
const joinRequest = await requestToJoin(org.id, { message: "Trabalho na região, posso entrar?" });
console.log(joinRequest.status); // "pending"

// O próprio usuário consulta o status da sua solicitação
const mine = await getMyJoinRequest(org.id); // null se nunca solicitou

// E pode cancelar enquanto estiver pendente
await cancelJoinRequest(org.id);

// Quem administra a organização (dono ou quem tem a permissão org:members:add)
const pending = await listJoinRequests(org.id);
await approveJoinRequest(org.id, joinRequest.id); // cria a membership (role default do app)
await rejectJoinRequest(org.id, joinRequest.id); // não cria membership

Solicitar entrada numa organização onde já existe uma solicitação rejected/cancelled reabre a mesma solicitação (volta pra pending) em vez de criar uma nova — não acumula histórico duplicado por usuário/organização.

Membros e convites

import { useMembers } from "@geekapps/auth-react-native";

const {
  listMembers,
  getMember,
  addMember,
  updateMember,
  removeMember,
  inviteMember,
  listInvites,
  revokeInvite,
} = useMembers();

const members = await listMembers(org.id); // OrganizationMember[] — cada um já traz `user` (nome/email/foto)
console.log(members[0].user.name, members[0].user.email);

const member = await getMember(org.id, members[0].id);

// `user` só existe aqui — colegas de organização. O SDK não expõe nenhuma
// forma de ler dados de um usuário que não seja membro da mesma org que
// você está consultando (nada de telefone/endereço/sessões, também).

// Adiciona um usuário que já existe pelo id (sem passar roleId, cai na role default do app)
await addMember(org.id, { userId: "user-uuid", roleId: "role-uuid" });

// Convida por email quem ainda não tem conta/membership
const invite = await inviteMember(org.id, { email: "[email protected]", roleId: "role-uuid" });

await updateMember(org.id, memberId, { roleId: "outra-role-uuid" });
await removeMember(org.id, memberId);

const invites = await listInvites(org.id); // pendentes, aceitos, expirados, revogados
await revokeInvite(org.id, invite.id);

Convite por email (redirectUri obrigatório)

redirectUri é sempre obrigatório ao convidar — o auth-server nunca hospeda uma tela de convite, então este app é quem trata o ?invite_token=... recebido nessa URI. Ela precisa estar cadastrada em Application.redirectUris, mesma validação do /authorize, sem exceção.

await inviteMember(org.id, {
  email: "[email protected]",
  roleId: "role-uuid",
  redirectUri: "myapp://invite", // precisa estar em Application.redirectUris
  emailTitle: "Você foi chamado!",         // opcional — padrão: "Entre em {organização}"
  emailSubtitle: "Time de vendas",          // opcional
  emailMessage: "Estamos ansiosos pra te ter no time.", // opcional
});

Deep link (myapp://invite) é o de costume pra apps React Native — o próprio SDK já escuta Linking e popula pendingInviteToken automaticamente (ver seção abaixo). redirectUri também pode ser uma URL https:// normal (útil se o convite for aceito por uma página web do seu próprio backend em vez do app).

Roles customizadas

import { useRoles } from "@geekapps/auth-react-native";

const { listRoles, getRole, createRole, updateRole, deleteRole } = useRoles();

const roles = await listRoles(org.id); // inclui roles do sistema e customizadas do app

const role = await createRole(org.id, {
  key: "editor",
  name: "Editor",
  permissionIds: ["perm-uuid-1", "perm-uuid-2"],
});

await updateRole(org.id, role.id, { permissionIds: ["perm-uuid-1"] });
await deleteRole(org.id, role.id);

Todas as chamadas de organização/membros/roles passam pelo mesmo request() autenticado do useAuth() — exigem sessão ativa e respeitam as permissões do usuário logado na organização (org:read, org:members:add, org:roles:update, etc. — verificadas pelo Auth Server, não pelo SDK).

Convites por email (aceitar direto no app)

O link do email de convite (redirectUri, deep link do app, ex: myapp://invite) tenta abrir o app diretamente com fallback pra loja se não estiver instalado. useInvites() (ou os mesmos campos em useAuth()) é como este app trata o ?invite_token=... recebido.

import { useEffect } from "react";
import { useAuth, useInvites } from "@geekapps/auth-react-native";

function InviteHandler() {
  const { pendingInviteToken, clearPendingInvite, isAuthenticated, signIn } = useAuth();
  const { getInvitePreview, acceptInvite, rejectInvite } = useInvites();

  useEffect(() => {
    if (!pendingInviteToken) return;
    // navegue pra uma tela própria de convite, passando pendingInviteToken
  }, [pendingInviteToken]);

  async function handleOpen(token: string) {
    const preview = await getInvitePreview(token);
    if (preview.alreadyAccepted) return; // já é membro

    if (!isAuthenticated) {
      await signIn(); // volta com sessão ativa; getInvitePreview de novo já traz currentAccount
      return;
    }

    // usuário logado — currentAccount presente, mostra tela de aceitar/recusar
  }

  async function handleAccept(token: string) {
    const result = await acceptInvite(token);
    if (result.success) {
      clearPendingInvite();
      console.log(`Entrou em ${result.organizationName}`);
    } else if (result.loginRequired) {
      await signIn();
    } else if (result.wrongAccount) {
      console.log(`Convite é para ${result.invitedEmail}, logado como ${result.currentEmail}`);
    }
  }

  async function handleReject(token: string) {
    await rejectInvite(token); // não exige estar logado
    clearPendingInvite();
  }

  return null;
}

pendingInviteToken é populado automaticamente sempre que o app recebe um deep link com ?invite_token=... — inclusive com o app já aberto e logado, ou como cold start (Linking.getInitialURL()). Chame clearPendingInvite() depois de tratar o convite (ex: ao navegar pra fora da tela).

Configuração necessária na Application (dashboard)

  • pkceFlowEnabled = true — permite redirect_uri com custom scheme (ex: myapp://callback).
  • Cadastrar myapp://callback na lista de redirect_uris da Application.
  • Pra aceitar convites direto no app, cadastrar também o deep link usado no convite (ex: myapp://invite) e configurar appStoreUrl/playStoreUrl da Application (fallback quando o app não está instalado).