Compare commits

...

8 commits

Author SHA1 Message Date
Adam Moussa
042339948c Add app icons and CFBundleIconName for TestFlight validation
Some checks are pending
Deploy / Deploy to AWS (push) Waiting to run
App Store Connect rejected the upload because:
- Missing 120x120px app icon (and other required sizes)
- Missing CFBundleIconName in Info.plist

Added placeholder solid-color icons at all required sizes.
These should be replaced with the actual Sea Haven logo.
2026-05-19 19:32:38 -04:00
Adam Moussa
fa93870a99 Fix RN bundle phase: align metro-config and babel-preset with RN 0.85
The "Bundle React Native code and images" Xcode build phase failed
because @react-native/metro-config and @react-native/babel-preset were
pinned to 0.79.x while react-native is 0.85.3. The 0.79 metro-config
API is incompatible with the 0.85 bundler script.

Also fixes xcodebuild_formatter from "" to "cat" to avoid empty pipe.
2026-05-19 19:21:33 -04:00
Adam Moussa
ddb1e52e68 Fix .xcode.env.local path: Fastlane runs from fastlane/ not mobile/ 2026-05-19 18:57:05 -04:00
Adam Moussa
8b997b9086 Fix NODE_BINARY for Xcode build phases in CI
Write .xcode.env.local with explicit node path so the "Bundle React
Native code and images" build phase can find node. Xcode build phases
don't inherit the GitHub Actions PATH. Also disable xcbeautify for
this run to see raw xcodebuild output for debugging.
2026-05-19 18:54:42 -04:00
Adam Moussa
0326909e51 Add verbose match output and keychain diagnostics for signing issue
Certificate installs to keychain but security find-identity shows
no signing identities. Added verbose match, explicit keychain params,
setup_ci force, profile_name in update_code_signing_settings, and
diagnostic security find-identity commands to diagnose the import.
2026-05-19 18:47:09 -04:00
Adam Moussa
69f582f0b3 Configure manual code signing for CI builds
Add update_code_signing_settings to disable automatic signing and
set development team (9KAQYC653W) + Apple Distribution identity.
Fixes Xcode error "Signing requires a development team" in CI.
2026-05-19 18:43:53 -04:00
Adam Moussa
a1f2e22562 Add headerless-body and DER-wrap strategies to ASC key normalizer
The .p8 file from Apple has no PEM headers, just the raw base64
body. Add strategies for: headerless body wrapped with PEM headers,
base64-decoded DER re-wrapped as PEM, and double-decoded DER. Also
show hex bytes in diagnostics for better binary data analysis.
Secret has been re-set with properly PEM-wrapped + base64-encoded
content matching the reusable workflow's expected format.
2026-05-19 18:40:29 -04:00
Adam Moussa
279cd6c94a Robust ASC key normalization with diagnostics for TestFlight deploy
Replace fragile BEGIN/base64 branch with multi-strategy key parser
that handles raw PEM, escaped newlines, base64-encoded PEM, mangled
line wrapping, CR/LF issues, and double-encoding. Validates key with
OpenSSL::PKey.read before passing to Fastlane via key_filepath (temp
file) instead of key_content to bypass Fastlane's own parsing. Prints
safe diagnostics (no key material) if all strategies fail.
2026-05-19 18:32:46 -04:00
14 changed files with 346 additions and 1174 deletions

1
.gitignore vendored
View file

@ -44,6 +44,7 @@ TestResults/
# React Native / Mobile # React Native / Mobile
mobile/ios/Pods/ mobile/ios/Pods/
mobile/ios/build/ mobile/ios/build/
mobile/ios/.xcode.env.local
mobile/android/.gradle/ mobile/android/.gradle/
mobile/android/app/build/ mobile/android/app/build/
mobile/android/build/ mobile/android/build/

View file

@ -1,8 +1,9 @@
require 'openssl' require 'openssl'
require 'base64'
require 'tempfile'
# Fastlane 2.234.0 uses OpenSSL::PKey::EC.new which rejects PKCS#8 keys on # OpenSSL 3.x rejects PKCS#8 keys via EC.new ("invalid curve name").
# OpenSSL 3.x ("invalid curve name"). Prepend a wrapper that falls back to # Prepend a wrapper that falls back to PKey.read for both formats.
# OpenSSL::PKey.read, which handles both PKCS#8 and SEC1 formats.
module OpenSSLECNewFix module OpenSSLECNewFix
def new(arg = nil, *rest) def new(arg = nil, *rest)
super super
@ -13,30 +14,124 @@ module OpenSSLECNewFix
end end
OpenSSL::PKey::EC.singleton_class.prepend(OpenSSLECNewFix) OpenSSL::PKey::EC.singleton_class.prepend(OpenSSLECNewFix)
def normalize_p8_key(raw)
candidates = []
cleaned = raw.gsub("\r", "")
with_newlines = cleaned.gsub('\n', "\n")
candidates << ["raw (newlines normalized)", with_newlines]
if with_newlines.include?("BEGIN")
b64_body = with_newlines.gsub(/-----(?:BEGIN|END)[^-]+-----/, '').gsub(/\s+/, '')
rewrapped = "-----BEGIN PRIVATE KEY-----\n#{b64_body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n"
candidates << ["rewrapped PEM", rewrapped]
end
unless with_newlines.include?("BEGIN")
body = cleaned.strip.gsub(/\s+/, '')
pem = "-----BEGIN PRIVATE KEY-----\n#{body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n"
candidates << ["headerless body → PEM", pem]
end
begin
decoded = Base64.decode64(cleaned.strip)
if decoded.include?("BEGIN")
decoded_clean = decoded.gsub("\r", "").gsub('\n', "\n")
candidates << ["base64→PEM", decoded_clean]
b64_body = decoded_clean.gsub(/-----(?:BEGIN|END)[^-]+-----/, '').gsub(/\s+/, '')
rewrapped = "-----BEGIN PRIVATE KEY-----\n#{b64_body.scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n"
candidates << ["base64→PEM rewrapped", rewrapped]
elsif decoded.length.between?(32, 256)
candidates << ["base64→DER", decoded]
der_pem = "-----BEGIN PRIVATE KEY-----\n#{Base64.strict_encode64(decoded).scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n"
candidates << ["base64→DER→PEM", der_pem]
end
rescue StandardError
# not valid base64
end
begin
double = Base64.decode64(Base64.decode64(cleaned.strip).strip)
if double.include?("BEGIN")
candidates << ["double-base64→PEM", double.gsub("\r", "")]
elsif double.length.between?(32, 256)
der_pem = "-----BEGIN PRIVATE KEY-----\n#{Base64.strict_encode64(double).scan(/.{1,64}/).join("\n")}\n-----END PRIVATE KEY-----\n"
candidates << ["double-base64→DER→PEM", der_pem]
end
rescue StandardError
# not double-encoded
end
candidates.each do |name, content|
begin
OpenSSL::PKey.read(content)
UI.success("ASC key parsed with strategy: #{name}")
return content
rescue StandardError => e
UI.message("Strategy '#{name}' failed: #{e.class} — #{e.message}")
end
end
UI.error("=== ASC KEY DIAGNOSTIC (no key material shown) ===")
UI.error("Raw byte length: #{raw.bytesize}")
UI.error("Starts with BEGIN: #{raw.strip.start_with?('-----BEGIN')}")
UI.error("Ends with -----: #{raw.strip.end_with?('-----')}")
UI.error("Has real newlines: #{raw.include?("\n")}")
UI.error("Has literal backslash-n: #{raw.include?('\\n')}")
UI.error("Has carriage returns: #{raw.include?("\r")}")
UI.error("Printable ASCII ratio: #{(raw.count(' -~').to_f / raw.bytesize * 100).round(1)}%")
UI.error("Header (first 27 chars): #{raw[0..26]}")
begin
d = Base64.decode64(raw.strip)
hex = d.bytes[0..15].map { |b| format('%02x', b) }.join(' ')
UI.error("After base64 decode — length: #{d.bytesize}, first 16 bytes hex: #{hex}")
rescue StandardError
UI.error("base64 decode raised an exception")
end
UI.error("=== END DIAGNOSTIC ===")
raise "Could not parse ASC_KEY_CONTENT in any known format. See diagnostics above."
end
default_platform(:ios) default_platform(:ios)
platform :ios do platform :ios do
desc "Build and upload to TestFlight" desc "Build and upload to TestFlight"
lane :beta do lane :beta do
setup_ci setup_ci(force: true)
raw = ENV["ASC_KEY_CONTENT"] key_pem = normalize_p8_key(ENV["ASC_KEY_CONTENT"])
if raw.include?("BEGIN")
key_content = raw.gsub('\n', "\n") key_path = File.join(Dir.tmpdir, "asc_api_key.p8")
is_base64 = false File.write(key_path, key_pem)
else
key_content = raw
is_base64 = true
end
app_store_connect_api_key( app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"], key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"], issuer_id: ENV["ASC_ISSUER_ID"],
key_content: key_content, key_filepath: key_path
is_key_content_base64: is_base64
) )
match(type: "appstore", readonly: true) File.delete(key_path) if File.exist?(key_path)
match(
type: "appstore",
readonly: true,
keychain_name: "fastlane_tmp_keychain",
keychain_password: ""
)
update_code_signing_settings(
use_automatic_signing: false,
team_id: "9KAQYC653W",
code_sign_identity: "Apple Distribution",
profile_name: "match AppStore com.seahavenind.proposals",
path: "ios/ProposalSystem.xcodeproj"
)
node_path = sh("which node").strip
xcode_env_path = File.join(__dir__, "..", "ios", ".xcode.env.local")
File.write(xcode_env_path, "export NODE_BINARY=#{node_path}\n")
UI.message("NODE_BINARY set to #{node_path} at #{xcode_env_path}")
increment_build_number( increment_build_number(
build_number: ENV["GITHUB_RUN_NUMBER"], build_number: ENV["GITHUB_RUN_NUMBER"],
@ -48,8 +143,10 @@ platform :ios do
scheme: "ProposalSystem", scheme: "ProposalSystem",
configuration: "Release", configuration: "Release",
export_method: "app-store", export_method: "app-store",
export_team_id: "9KAQYC653W",
output_directory: "build", output_directory: "build",
output_name: "ProposalSystem.ipa" output_name: "ProposalSystem.ipa",
xcodebuild_formatter: "cat"
) )
upload_to_testflight(skip_waiting_for_build_processing: true) upload_to_testflight(skip_waiting_for_build_processing: true)

View file

@ -1,46 +1,55 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "Icon-40.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "Icon-60.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "20x20" "size" : "20x20"
}, },
{ {
"filename" : "Icon-58.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "Icon-87.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "29x29" "size" : "29x29"
}, },
{ {
"filename" : "Icon-80.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "Icon-120.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "40x40" "size" : "40x40"
}, },
{ {
"filename" : "Icon-120.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "2x", "scale" : "2x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "Icon-180.png",
"idiom" : "iphone", "idiom" : "iphone",
"scale" : "3x", "scale" : "3x",
"size" : "60x60" "size" : "60x60"
}, },
{ {
"filename" : "Icon-1024.png",
"idiom" : "ios-marketing", "idiom" : "ios-marketing",
"scale" : "1x", "scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

View file

@ -84,5 +84,7 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>CFBundleIconName</key>
<string>AppIcon</string>
</dict> </dict>
</plist> </plist>

1375
mobile/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,8 +35,8 @@
"devDependencies": { "devDependencies": {
"@react-native-community/cli": "^20.1.3", "@react-native-community/cli": "^20.1.3",
"@react-native-community/cli-platform-ios": "^20.1.3", "@react-native-community/cli-platform-ios": "^20.1.3",
"@react-native/babel-preset": "^0.79.0", "@react-native/babel-preset": "^0.85.3",
"@react-native/metro-config": "^0.79.0", "@react-native/metro-config": "^0.85.3",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-native-vector-icons": "^6.4.18", "@types/react-native-vector-icons": "^6.4.18",
"typescript": "~5.7.0" "typescript": "~5.7.0"