Skip to content

Beta-ready: modo competição + rename Cockpit + 8 fixes de UI - #5

Open
cardos0s wants to merge 20 commits into
mainfrom
fix/live-perf
Open

Beta-ready: modo competição + rename Cockpit + 8 fixes de UI#5
cardos0s wants to merge 20 commits into
mainfrom
fix/live-perf

Conversation

@cardos0s

Copy link
Copy Markdown
Owner

Resumo

PR grande que consolida tudo do fix/live-perf pra mergear no main e destravar o beta. São 16 commits agrupados em 4 frentes:

🏁 Nova feature: Modo Competição (5 commits)

Eventos multi-piloto com ranking ao vivo + posição no mapa pra audiência.

  • 4e49f61 Schema + APIs (tabela events, FKs em live_sessions/laps/samples)
  • a38d64a UI no app: criar/entrar em evento via código de 6 chars
  • 6d7761a Web /event/[code] com ranking ao vivo
  • ee7f651 Ranking in-app + tela /ranking/[code]
  • 16d1ab4 Posição ao vivo no mapa (primeira volta vira referência ENU, projeção por sample)

🎨 Rebranding: Copilot → Cockpit (1 commit)

  • a10457b Nome no app.config.js, strings de permissão, web layout.tsx e page.tsx, notificação de gravação. Bundle ID com.cortextech.copilot mantido pra preservar dados locais dos testers.

🛡️ Robustez de campo (3 commits)

  • 0c6126f Pista custom — destrava qualquer kartódromo fora da lista (migration SQLite v3→v4 + custom_tracks table)
  • 6603a57 Auto-save anti-crash via expo-file-system (snapshot a cada 30s + ao fechar volta; recovery na inicialização)
  • 3dea91b Backup anônimo de sessões na nuvem por device_id (sem login)

🐛 8 fixes críticos

  • 1917cd5 5 telas com loading eterno (leaderboard, challenges, career, profile-edit, insights) — try/finally + .catch por fonte
  • 8edd6e6 Tela de resultados (session/[id]) idem
  • 716d3be New-session idem (custom_tracks throwing)
  • 8f8b899 GPS quantizado a segundos → tempos redondos (42.000, 44.000). Fix prioriza Date.now() quando loc.timestamp % 1000 === 0
  • 9c227b3 Painel da equipe: VOLTA crescia sem parar (mandava info.elapsedMs em vez de currentLapElapsedMs); DELTA estático (mandava delta de PB em vez de liveDeltaMs)
  • 100f2fb Formato MM:SS.SSS padronizado em todo lugar
  • b617ce2 Painel da equipe não trava mais em sessões longas (cap em estado + decimação no render SVG)
  • 4365119 Reverte batching que quebrou data flow

Mudanças no schema do Supabase

Já rodadas pelo dev no SQL editor:

  • Tabela events + RLS + realtime
  • Colunas event_id em live_sessions, live_laps, live_samples
  • Colunas reference_samples_json, reference_duration_ms, reference_set_at em events
  • Índices em event_id (laps e samples)

Idempotente — pode rodar n vezes sem efeito colateral.

Estatísticas

  • 35 arquivos modificados
  • +2901 / -165 linhas
  • App + web + schema

⚠️ Pendências conhecidas (não bloqueiam beta privado)

  1. RLS using (true) em todas as tabelas do Supabase — qualquer um com o código pode inserir dados. OK pra beta privado, BLOQUEIA release público.
  2. Código de evento de 6 chars — brute-forceável (~15min). Aumentar pra 10 chars antes do beta aberto.
  3. Sem Sentry — crashes não são visíveis. Adicionar antes do beta.
  4. Cron de cleanup existe na função mas nunca foi agendado. Agendar a cada 6h pra não estourar tier free.

Test plan

  • Web copilot-mu-eight.vercel.app mostra Cockpit Live + modo competição
  • APK 25d79ab4 instala como Cockpit e abre sem crash
  • Nova sessão não trava em "carregando" (fix do custom_tracks)
  • Ver resultados de sessão não trava em "carregando" (fix do session/[id])
  • Validar fix dos outros 5 hangs (APK novo em build)
  • Testar fluxo end-to-end competição na pista real

cardos0s added 18 commits May 24, 2026 23:09
Sintoma: dashboard web da equipe travava após 2-5min de sessão.
Causa: setState a cada INSERT do realtime (10Hz GPS) = 10 re-renders/seg
+ samples crescendo sem cap (3000+ pontos no SVG) = death spiral.

== Fix lado web (web-spectator/lib/useLive.ts) ==
- pendingSamplesRef + flush timer (400ms): samples vindos do realtime
  vão pra um ref, e um único setState ocorre a cada 400ms com todos os
  samples acumulados. Re-renders caem de 10/s pra 2.5/s (-75%).
- MAX_SAMPLES_IN_STATE = 3000: cap nos samples mantidos no state. Em
  sessões muito longas (>5min @ 10Hz) os mais antigos saem do array
  pra bound memória. Dado continua salvo no Supabase.

== Fix render do traçado (team/[code]/page.tsx) ==
- TrackPanel agora decima samples pra max 500 pontos ANTES de
  projectTrack + render do <path>. Mantém formato visual da pista
  intacto (oval/circuito reconhecível) mas reduz custo do SVG em ~6x.
- Garante que último sample sempre tá incluído (continuidade).

== Fix lado piloto (app/recording.tsx) ==
- PUBLISH_DECIMATION = 3: publica 1 a cada 3 samples = ~3.3Hz efetivo
  em vez de 10Hz. Coaching ao vivo não precisa de 10Hz; 3-4Hz dá
  posição precisa o suficiente.
- Fire-and-forget (sem await no loop): se uma INSERT engasgar por
  rede ruim, a próxima não espera. Antes uma INSERT lenta atrasava
  todas as seguintes.

Combinação: ~12x menos load no painel (decimation no publish 3x ×
batch no realtime 4x × decimation no render 6x quando aplicável).
Pra sessão de 20min: 60s × 20 × 3.3Hz × cap = ~3000 samples no state
máximo, sempre.
Sintoma reportado: depois do deploy do batching, painel da equipe
parava de atualizar velocidade/tempo após ~2 updates. Speed/time
estagnavam mesmo com sample ainda chegando do realtime.

O batching com pendingSamplesRef + flushTimer setInterval introduziu
uma camada extra de complexidade onde algo dropava em certas condições
(hipóteses: timer não re-armando, closure stale, race com cleanup).
Sem visibilidade do log do browser, mais seguro reverter.

Volta pra setState a cada INSERT do realtime (10Hz) MAS com cap em
MAX_SAMPLES_IN_STATE = 3000 aplicado na própria operação (shift + push
quando estoura). Garante:
- UI continua "viva" 10x/seg (igual antes do batching)
- Memória bounded em sessão longa (>5min)
- Custo do SVG render bounded pela decimação que já existe no
  TrackPanel (max 500 pontos no path)

Net: cap + decimation = melhoria similar ao batching, sem o risco de
quebrar data flow. Se ainda lagar em sessão >10min, aí volto pra
batching com testes mais cuidadosos.
Antes tinha inconsistência:
- App HUD do kart (recording.tsx fmtLapShort): "34.872" pra voltas <60s,
  sem minuto. Difícil ler "tempo total" rápido — usuário não sabe se é
  34s ou 1m34s.
- Web format.ts fmtLap: "0:34.872" (1 dígito minuto). OK, mas inconsistente
  com sessions.tsx do app que já fazia "00:34.872".
- Replay screen tinha formatador próprio com 2 decimais ("34.87") — perdia
  precisão de milésimo.

Agora padrão único em tudo (motorsport standard, MyChron/Speedhive):
  MM:SS.SSS (minuto sempre 2 dígitos, milésimo sempre 3 dígitos)

Exemplo: 34.872s → "00:34.872", 1m23.456s → "01:23.456"

Centésimos ficam embutidos nos 2 primeiros dígitos do milésimo:
"00:34.876" = 34s + 87 centésimos + 876 milésimos.

Arquivos:
- web-spectator/lib/format.ts: fmtLap + fmtTime ganham padStart no minuto
- app/recording.tsx: removido fmtLapShort, todos os usos viraram fmtLap
- app/replay/[id].tsx: fmtTime local reescrito pro formato padrão
Dois bugs no publishSample (app → realtime → painel):

1. lapElapsedMs publicava info.elapsedMs (tempo TOTAL da sessão) em vez
   de info.currentLapElapsedMs (tempo da volta atual). Resultado: o
   campo "VOLTA" no painel mostrava 8:35 crescendo sem parar, em vez
   de resetar a cada volta (0:01 → 0:42 → reset).

2. deltaVsRefMs publicava um valor ESTÁTICO (info.bestLapMs −
   reference.durationMs) que só mudava ao bater PB. Por isso o
   "DELTA · LIVE" ficava congelado (+1.000s no print). Agora publica
   info.liveDeltaMs — o delta MyChron real no ponto atual da pista,
   que o hook já calcula a cada sample.

Deps do useEffect atualizadas (info.currentLapElapsedMs, info.liveDeltaMs).

Precisa rebuild do APK pra valer (mudança no app, não no web).
Raiz do bug "ÚLTIMA travada + tempos redondos" reportado em pista real:
o GPS de alguns Android entrega loc.timestamp QUANTIZADO a segundos
cheios (sempre múltiplo de 1000ms). Como durationMs = sample[fim].t −
sample[início].t, os tempos de volta saíam sempre redondos (42.000,
43.999) — sem centésimo/milésimo. E voltas consecutivas com valores
quase idênticos pareciam "travadas" no painel da equipe.

Fix no BG task: prefere Date.now() (precisão de ms, fica a poucos ms do
tempo real do fix porque o task roda quase em tempo real gravando) quando
loc.timestamp vem quantizado ou ausente. Heurística por sample:
  - loc.timestamp com precisão sub-segundo (% 1000 != 0) → confia nele
    (devices bons mantêm comportamento atual)
  - quantizado/0/ausente → Date.now() com spread intra-batch (~100ms/
    sample retroativo) pra batch com várias locations não colidir no
    mesmo t. No caso comum (1 location/call em foreground), t = Date.now()
    distinto por call = timing real preservado.

Resolve os 2 sintomas de uma vez: tempos precisos + ÚLTIMA deixa de
"travar" (valores passam a variar de verdade volta a volta).

Precisa rebuild do APK (mudança no app).
Bloqueador #1 pra teste de campo: só havia 8 pistas hardcoded. Quem
estivesse num kartódromo fora da lista não conseguia nem começar uma
sessão. Agora dá pra criar a pista na hora.

- src/storage/db.ts: migration v3→v4 cria tabela custom_tracks.
  CRUD: listCustomTracks, addCustomTrack (id "custom-<ts>-<rand>"),
  deleteCustomTrack. Importa TrackRef de data/tracks.
- src/data/tracks.ts: cache em memória (customTracksCache) +
  setCustomTracksCache / getCustomTracksCached / getAllTracks /
  isCustomTrack. findTrackById agora checa hardcoded E custom — mantém
  SÍNCRONO (muitos call sites chamam sem await). Cache hidratado no boot.
- app/_layout.tsx: hidrata o cache no boot (listCustomTracks →
  setCustomTracksCache), falha silenciosa.
- app/new-track.tsx (novo): form de criar pista — nome (obrigatório),
  cidade/UF (opcional), captura GPS atual (Location.getCurrentPosition).
  Salva no DB + re-hidrata cache + volta.
- app/new-session.tsx: lista agora usa getAllTracks() (hardcoded +
  custom). Re-hidrata cache no load. Botão tracejado "Minha pista não
  está aqui" → /new-track.

Pista custom funciona igual hardcoded daí pra frente: escolhe traçado,
grava reconhecimento, etc. Precisa rebuild do APK (mudança no app).
Antes: samples viviam só em memória (allSamplesRef) até o Encerrar.
Crash / app morto pelo SO / bateria acabar no meio = perde tudo.

Agora: snapshot do estado bruto (samples + IMU + metadata) num arquivo
JSON a cada volta fechada + a cada 30s. Encerramento limpo apaga o
arquivo. No boot, se o arquivo existe → houve interrupção → home mostra
banner pra recuperar ou descartar.

- src/storage/recovery.ts (novo): saveRecoverySnapshot / loadRecoverySnapshot
  / hasRecoverySnapshot / clearRecoverySnapshot via expo-file-system
  (documentDirectory, sobrescreve arquivo único). recoverSnapshotToSession:
  roda detectLaps nos samples brutos, cria session + salva voltas com
  samples + IMU recortados por timestamp. Versão enxuta (sem gamification/
  IA) — o que importa é não perder o dado.
- src/hooks/useLapRecorder.ts: start(recoveryMeta?) recebe metadata da
  sessão. Poll grava snapshot on lap close + a cada 30s (fire-and-forget,
  não bloqueia). stop() limpa o snapshot (encerramento limpo).
- app/recording.tsx: handleStart passa metadata (trackId/Name, layoutId,
  kartSetupId, mode) pro start().
- app/(tabs)/index.tsx: banner de recuperação no topo da home quando há
  snapshot. Botões Recuperar (reconstrói + abre a sessão) / Descartar.

Migration: nenhuma no SQLite (usa filesystem). Precisa rebuild do APK.
Opção B (enxuta): sincroniza metadata + tempos de volta pro Supabase
por device_id. Backup pro tester (não perde histórico ao reinstalar/
trocar de celular) + visibilidade pro dev (ver uso de campo dos testers
sem precisar pegar o celular deles). Sem login = zero fricção.

NÃO sincroniza sample arrays brutos (GPS/IMU) — grandes demais pro free
tier. Só metadata + tempos. Telemetria completa fica local.

- supabase/schema.sql: tabelas synced_sessions (metadata + best_lap +
  lap_count) e synced_laps (tempos por volta + colunas de setor prontas).
  Upsert idempotente via UNIQUE(device_id, local_session_id[, lap_number]).
  RLS permissivo + policies idempotentes.
- src/lib/sessionSync.ts (novo): syncSession (upsert 1 sessão + voltas) +
  syncAllSessions (backfill das 50 mais recentes). Respeita toggle de
  opt-out. Falha silenciosa — nunca quebra o fluxo (funciona offline).
- src/storage/preferences.ts: getCloudSyncEnabled/setCloudSyncEnabled
  (default ON).
- app/recording.tsx: syncSession fire-and-forget após salvar a sessão.
- app/_layout.tsx: syncAllSessions no boot (background).
- app/settings.tsx: toggle "Sincronizar sessões" em BACKUP NA NUVEM —
  opt-out de privacidade (LGPD: usuário pode recusar).

Dev vê os dados no Supabase Table Editor (synced_sessions / synced_laps)
por enquanto. Dashboard web dedicado pode vir depois.

Nota LGPD: pra produção, adicionar tela de consentimento explícito no
onboarding. O toggle default-ON + opt-out é um começo razoável pra teste
com pessoas conhecidas.

Precisa rodar o schema.sql atualizado no Supabase + rebuild do APK.
Modo competição: evento agrupa várias live_sessions (1 por piloto) sob
um código. Ranking ao vivo agrega a melhor volta de cada piloto.

Schema (supabase/schema.sql):
- Tabela events (code, name, track, created_by, expires 12h)
- live_sessions.event_id + live_laps.event_id (denormalizado pro
  realtime filtrar por evento)
- Realtime em events + RLS/policies idempotentes + índices

APIs (src/lib/liveSession.ts):
- EventInfo + EventRankingRow; eventId em CreateOpts/LiveSessionInfo
- createEvent, findEventByCode, createLiveSession grava event_id
- loadEventRanking (agrega por piloto, ordena por melhor volta)
- subscribeEventLaps (realtime de voltas do evento)
- publishLap aceita eventId
CompetitionPanel na tela idle (abaixo do toggle de live):
- "Criar competição" → createEvent → live session nasce vinculada ao
  evento (eventId), mostra o código pra compartilhar
- "Entrar com código" → findEventByCode → idem
- Quando em evento: card destacado (ciano) com código + link do ranking web

Entrar/criar implica ativar live (a sessão precisa jorrar voltas pro
ranking). handleStartLive agora aceita eventId; publishLap manda eventId
(denormalizado) pra cada volta entrar no ranking via realtime.

handleStopLive limpa o event. Imports: ActivityIndicator + TextInput.
- web-spectator/lib/liveTypes.ts: EventInfo + EventRankingRow
- web-spectator/lib/useEventRanking.ts: carrega evento por código, agrega
  ranking (melhor volta por piloto, espelha a lógica do app), assina
  realtime (nova volta de qualquer piloto + piloto novo entrando) e
  recarrega debounced.
- web-spectator/app/event/[code]/page.tsx: leaderboard ao vivo —
  posição, piloto, kart#, melhor volta, gap pro líder, última, nº voltas.
  Líder destacado. Ideal num tablet/TV no box.
- web-spectator/lib/createEvent.ts + home: 3º modo "Competição" no
  seletor (Espectador/Equipe/Competição). Ver ranking por código OU
  "+ Criar nova competição" (organizador que não corre cria pela web).
- app/ranking/[code].tsx (novo): leaderboard nativo da competição. Resolve
  código → evento → ranking agregado, assina realtime (subscribeEventLaps)
  e recarrega debounced a cada volta nova. Líder destacado, gap pro líder,
  última volta, contagem. Pro piloto ver posição entre voltas no box.
- app/recording.tsx: CompetitionPanel (estado "na competição") ganha botão
  "Ver ranking ao vivo" → /ranking/[code].

Modo Competição completo (waves 1-4):
  1. Backend (events + APIs + ranking agregado)
  2. App: criar/entrar evento na idle, voltas publicam com event_id
  3. Web: /event/[code] ranking + criar competição na home
  4. App: tela de ranking nativa

Fluxo: organizador cria (app ou web) → pilotos entram pelo código →
cada um corre → ranking ao vivo agrega melhor volta de todos, no app
e na web.
Camada extra sobre o ranking de tempos: agora a /event/[code] mostra
também a ORDEM REAL DE CORRIDA (P1/P2/P3 na pista agora) + mapa SVG
com cada kart no ponto onde está.

Funciona mesmo SE ninguém fez reconhecimento — o 1º piloto a fechar
volta no evento define a referência (auto), todos os karts são
projetados nela daí pra frente.

== Schema (supabase/schema.sql) ==
- events ganha reference_samples_json + reference_duration_ms +
  reference_set_at (referência geográfica fixada na 1ª volta).
- live_samples ganha event_id (denormalizado pra realtime filtrado
  por evento — pega samples de todos os pilotos num canal só).
- Índice idx_live_samples_event.

== App ==
- src/lib/liveSession.ts: setEventReferenceIfEmpty (UPDATE atômico com
  WHERE reference_set_at IS NULL — só o 1º piloto ganha; demais no-op).
  publishSample agora aceita eventId.
- app/recording.tsx: publishSample manda live.eventId. Após publishLap
  no evento, tenta gravar essa volta como referência (decimada a ~500
  pontos pra ~15KB JSON, fire-and-forget).

== Web ==
- web-spectator/lib/trackProgress.ts (novo): compileReference (projeção
  ENU local + cumulativa + bbox) + projectProgress (projeção do GPS na
  polyline, retorna progresso 0..1).
- web-spectator/lib/useEventRanking.ts: carrega reference do evento,
  subscreve live_samples WHERE event_id, mantém mapa latestBySession,
  computa positions ordenadas por (lapNumber desc, progress desc).
  Throttle de 250ms nas atualizações de posição (samples a ~3.3Hz×N
  pilotos = barulho).
- web-spectator/app/event/[code]/page.tsx: nova seção lado-a-lado com
  TrackMap (SVG da pista + kart por piloto colorido) e LivePositionList
  (ordem na pista + nº volta + progresso %).

Limitações honestas:
- Precisão do GPS ~3-5m: lado a lado por centímetros não distingue.
- Posição aparece só DEPOIS da 1ª volta fechar (define a referência).
- Sem referência: ordena só por nº de voltas (karts na mesma volta
  ficam empatados).
Mudança só visível ao usuário. Identificadores internos (bundleId,
slug EAS, domínio Vercel) ficam estáveis pra não quebrar instalações
existentes nem o pipeline de build.

- app.config.js: name 'Copilot' → 'Cockpit'; permissões iOS de "O KartLap"
  pra "O Cockpit" (NSLocationWhenInUse, NSLocationAlwaysAndWhenInUse,
  NSMotionUsage + expo-location).
- src/hooks/useLapRecorder.ts: foregroundService.notificationTitle
  'Copilot gravando' → 'Cockpit gravando'.
- src/components/celebrations/PbUnlocked.tsx: mensagem de share PB.
- web-spectator/app/layout.tsx: <title>Cockpit Live</title>.
- web-spectator/app/page.tsx: <h1>Cockpit Live</h1>.

Comentários doc /** */ e tags de console.warn mantidos (não visíveis
ao usuário). Bundle ID com.cortextech.copilot mantido — quem já tem
APK instalado continua funcionando.
…falhasse

Sintoma: usuário tocava "Nova sessão" → tela ficava no spinner pra sempre.

Causa: load() usava Promise.all sem catch. Se a query listCustomTracks
(nova, da migration v4) lançasse erro — por tabela não existir ainda em
algum device (migration falhada) ou qualquer outro motivo —, o await
explodia e setLoading(false) nunca rodava.

Fix em duas camadas:
- src/storage/db.ts: listCustomTracks agora retorna [] em vez de lançar
  quando a tabela não existe ou a query falha. Pista custom só some da
  UI; resto da app continua.
- app/new-session.tsx: load() com try/finally — setLoading(false) sempre
  dispara mesmo se tudo falhar. Cada fonte do Promise.all tem .catch
  individual com fallback (Map vazio, null, []) — falha de uma não
  derruba as outras.
Sintoma: usuário tocava em uma sessão na lista pra ver os resultados →
tela ficava em spinner pra sempre.

Causa: mesmo padrão da new-session — o useEffect fazia vários await
encadeados (getSession, getLapsForSession, getLayout, getDefaultLayoutForTrack)
sem try/catch. Qualquer um lançando deixava setLoading(false) na linha 250
nunca rodar.

Cenários que disparam o bug:
- Sessão antiga apontando pra layoutId/trackId que não existe mais.
- DB com row corrompida (campo JSON inválido).
- Qualquer falha transitória do SQLite.

Fix: try/finally em volta do bloco inteiro garante setLoading(false), e
cada await individual tem .catch com fallback (null ou []) — uma fonte
falhar não derruba as outras, e a tela renderiza o estado vazio em vez
de travar.
Mesmo bug-pattern de new-session/session: useEffect com setLoading(true)
+ awaits sem try/finally. Qualquer await rejeitando deixava o spinner
eternamente.

Telas afetadas:
- app/leaderboard.tsx: fetchLeaderboard sem catch.
- app/challenges.tsx: refreshTodayChallenges + Promise.all sem catch.
- app/career.tsx: Promise.all sem catch (3 fontes).
- app/profile-edit.tsx: getProfile().then sem .catch.
- app/(tabs)/insights.tsx: computeSmartInsights sem catch.

Fix em duas camadas:
1. try/finally garante setLoading(false) mesmo em erro.
2. .catch por fonte com fallback ([], null, defaults) — falha de uma
   não derruba as outras; tela renderiza estado vazio em vez de travar.

Code review completo identificou também:
- Cap em useLapRecorder buffers (risco real, refactor não-trivial,
  defer).
- publishSample loop em recording.tsx (REFUTADO — já decima 3x).
- LIMIT em queries de laps (volume típico não justifica, defer).
- RLS supabase using(true) — defer pra refactor de auth.
@netlify

netlify Bot commented Jun 15, 2026

Copy link
Copy Markdown

Deploy Preview for relaxed-torte-d1c1b9 ready!

Name Link
🔨 Latest commit dfa5f51
🔍 Latest deploy log https://app.netlify.com/projects/relaxed-torte-d1c1b9/deploys/6a30aee4033cb60008e43098
😎 Deploy Preview https://deploy-preview-5--relaxed-torte-d1c1b9.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

cardos0s added 2 commits June 15, 2026 22:22
Antes de publicar no App Store. Bundle ID com nome antigo (copilot)
ficaria pra sempre — agora alinha com a marca Cockpit.

Impacto:
- iOS sai de cara com bundle correto (nunca publicado, sem impacto).
- Android: usuarios do APK atual perdem dados locais ao instalar
  proxima versao (bundle diferente = storage diferente). Hoje sao
  basicamente eu + 1-2 testers; aceitavel.
- App Store Connect: usar este bundle ao criar o app.
- Apple Developer: registrar com.cortextech.cockpit nos Identifiers.
App usa apenas HTTPS via system TLS e Keychain do iOS — nenhuma crypto
custom ou lib extra. Declarando isso no Info.plist evita o dialog de
Export Compliance Documentation a cada build futuro.

Standard exempt under §740.17(b)(1) of the EAR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant