Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.facebook.react.uimanager.UIManagerHelper
*
* Row 1: [card icon] Card number (full width, rounded border)
* Row 2: Expiration | CVV (split 50/50 with divider, rounded border)
* Row 3: Billing zip (full width, hidden by default, rounded border)
* Row 3: Postal code (full width, hidden by default, rounded border)
*
* Row spacing: 16dp. Row height: 48dp. Corner radius: 10dp.
* Border: #d1d5db, 1dp. Background: #fafafa. Cursor: Bolt purple #5A31F4.
Expand Down Expand Up @@ -198,7 +198,7 @@ class BoltCardFieldView(context: Context) : LinearLayout(context) {
postalField.background = null
postalField.setTextColor(colorNormal)
postalField.setHintTextColor(Color.parseColor("#9ca3af"))
postalField.hint = "Billing zip"
postalField.hint = "Postal code"
postalField.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
postalField.gravity = Gravity.CENTER_VERTICAL
postalField.setPadding(dp(12), 0, dp(12), 0)
Expand Down
12 changes: 6 additions & 6 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- BoltReactNativeSdk (0.9.0):
- BoltReactNativeSdk (0.9.2):
- hermes-engine
- RCTRequired
- RCTTypeSafety
Expand Down Expand Up @@ -1754,9 +1754,9 @@ PODS:
- React-runtimeexecutor
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- ReactAppDependencyProvider (0.85.0):
- ReactAppDependencyProvider (0.85.2):
- ReactCodegen
- ReactCodegen (0.85.0):
- ReactCodegen (0.85.2):
- hermes-engine
- RCTRequired
- RCTTypeSafety
Expand Down Expand Up @@ -2050,7 +2050,7 @@ EXTERNAL SOURCES:
:path: "../../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
BoltReactNativeSdk: 73295af8005b194c7e540f5ec24c01052a407336
BoltReactNativeSdk: 39dc5824d68bad253daccec5c53ac3fb51ce96ad
FBLazyVector: 26fd21c75314e101f280d401e97f27d54f3f7064
hermes-engine: bd88c0774d9ce8cc1a486c246afaffcfdef8237f
RCTDeprecation: c7a2768f905d76ca6d2cfefb26e4349eacbdfca3
Expand Down Expand Up @@ -2121,8 +2121,8 @@ SPEC CHECKSUMS:
React-timing: c307da445abf09b4597796951f15118707dd99b5
React-utils: 76e964b4a6b480ea47cc22392595028a2a4c879b
React-webperformancenativemodule: f7cdde6e05a1f94e7dec4ff4956bdabfd544f2a5
ReactAppDependencyProvider: 706b65371b90b5cc797b6639e8979f2e5cecd6da
ReactCodegen: 7aad71c10c1852b664587c865175f73819fb9460
ReactAppDependencyProvider: 2201c845e5f506d5957669e66abb8de5ce20ad1f
ReactCodegen: bde8734501b3183c7a5e33be4473caaef8d715c4
ReactCommon: 376f3a275bcf5e59a360e0b1762beb3bd41495ce
ReactNativeDependencies: e7def8d3e9a65cecae96ee4713e8b4f9bc40281b
Yoga: df8920a8f8ca99996048f6aea23a529f44992eac
Expand Down
2 changes: 1 addition & 1 deletion ios/BoltPostalField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class BoltPostalField: BoltBaseField {
}

private func setup() {
placeholder = "Billing zip"
placeholder = "Postal code"
keyboardType = .default
autocorrectionType = .no
spellCheckingType = .no
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
],
"scripts": {
"example": "yarn workspace bolt-react-native-sdk-example",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib example/src/boltConfig.ts",
"prepare": "node scripts/update-sdk-version.js && bob build",
"typecheck": "tsc",
"test": "jest",
Expand Down
20 changes: 9 additions & 11 deletions scripts/gen-bolt-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
* Generates example/src/boltConfig.ts from environment variables or a .env file.
* The generated file is gitignored — never committed.
*
* If boltConfig.ts already exists, this script does nothing (preserves manual edits).
* Delete the file and re-run to regenerate.
*
* If BOLT_PUBLISHABLE_KEY is not set, copies the example template so the project
* still compiles (you'll need to fill in a real key to use the example app).
* If BOLT_PUBLISHABLE_KEY is set (env or .env), regenerates the file from those
* values. If not set and the file is missing, copies the example template so the
* project still compiles. If not set and the file exists, leaves it alone
* (preserves any manual edits you made directly in boltConfig.ts).
*
* Reads from:
* 1. Environment variables
Expand All @@ -30,11 +29,6 @@ const ENV_FILE = path.join(ROOT, '.env');
const OUT_FILE = path.join(ROOT, 'example', 'src', 'boltConfig.ts');
const EXAMPLE_FILE = path.join(ROOT, 'example', 'src', 'boltConfig.example.ts');

// Skip if already generated
if (fs.existsSync(OUT_FILE)) {
process.exit(0);
}

// Load .env file if present (simple key=value parser)
const loadDotEnv = () => {
if (!fs.existsSync(ENV_FILE)) return;
Expand All @@ -61,7 +55,11 @@ const publishableKey = process.env.BOLT_PUBLISHABLE_KEY;
const environment = process.env.BOLT_ENVIRONMENT ?? 'sandbox';

if (!publishableKey) {
// No key configured — copy the example template so the project compiles.
// No key configured. Leave existing file alone (preserves manual edits);
// otherwise copy the example template so the project still compiles.
if (fs.existsSync(OUT_FILE)) {
process.exit(0);
}
fs.copyFileSync(EXAMPLE_FILE, OUT_FILE);
console.log(
'No BOLT_PUBLISHABLE_KEY found. Copied boltConfig.example.ts → boltConfig.ts'
Expand Down
175 changes: 175 additions & 0 deletions src/__tests__/BoltBridgeDispatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,47 @@ describe('BoltBridgeDispatcher', () => {
expect(mockWebView.injectJavaScript).toHaveBeenCalled();
});

it('sendBootstrapPort emits a postMessage envelope with virtualPortId', () => {
const { dispatcher, injectedScripts } = createDispatcher();
dispatcher.handleMessage(makeBridgeReadyEvent());

dispatcher.sendBootstrapPort('vp_test', { type: 'setPort', payload: 'x' });

expect(injectedScripts).toHaveLength(1);
const match = injectedScripts[0]!.match(/__boltBridgeReceive\((".*")\)/s);
expect(match).not.toBeNull();
const inner = JSON.parse(match![1]!) as string;
const envelope = JSON.parse(inner);
expect(envelope).toMatchObject({
__boltBridge: true,
type: 'postMessage',
virtualPortId: 'vp_test',
data: { type: 'setPort', payload: 'x' },
});
});

it('sendBootstrapPort queues until bridge ready', () => {
const { dispatcher, injectedScripts } = createDispatcher();
dispatcher.sendBootstrapPort('vp_test', { type: 'setPort' });
expect(injectedScripts).toHaveLength(0);

dispatcher.handleMessage(makeBridgeReadyEvent());
expect(injectedScripts).toHaveLength(1);
});

it('onReady listener fires on every transition to ready', () => {
const { dispatcher } = createDispatcher();
const onReady = jest.fn();
dispatcher.onReady(onReady);

dispatcher.handleMessage(makeBridgeReadyEvent());
expect(onReady).toHaveBeenCalledTimes(1);

dispatcher.reset();
dispatcher.handleMessage(makeBridgeReadyEvent());
expect(onReady).toHaveBeenCalledTimes(2);
});

it('should not throw when sending message with no webView', () => {
const ref = { current: null };
const dispatcher = new BoltBridgeDispatcher(ref as any);
Expand Down Expand Up @@ -296,6 +337,140 @@ describe('INJECTED_BRIDGE_JS', () => {
});
});

describe('INJECTED_BRIDGE_JS — VirtualMessagePort runtime', () => {
// Evaluate the injected script in a sandboxed window so we can drive the
// virtual port directly. The script bails out unless window.parent ===
// window, so we set that up before evaluating.
const makeSandbox = () => {
const sandbox: any = {};
sandbox.window = sandbox;
sandbox.self = sandbox;
sandbox.parent = sandbox;
sandbox.location = { origin: 'https://iframe.test' };
sandbox.document = {
readyState: 'complete',
addEventListener: () => {},
referrer: '',
};
sandbox.ReactNativeWebView = { postMessage: jest.fn() };
sandbox.console = { error: jest.fn(), log: jest.fn(), warn: jest.fn() };
sandbox.addEventListener = () => {};
sandbox.removeEventListener = () => {};
// The script references bare `document` and `console` (e.g.
// document.readyState, console.error). Pass them as function
// parameters so lookups resolve locally instead of escaping to the
// test's global scope.
// eslint-disable-next-line no-new-func
new Function('window', 'document', 'console', INJECTED_BRIDGE_JS)(
sandbox,
sandbox.document,
sandbox.console
);
return sandbox;
};

const capturePort = (sandbox: any, portId: string) => {
let port: any = null;
sandbox.addEventListener('message', (event: any) => {
if (event.ports && event.ports[0]) port = event.ports[0];
});
sandbox.__boltBridgeReceive(
JSON.stringify({
__boltBridge: true,
type: 'postMessage',
virtualPortId: portId,
data: null,
})
);
return port;
};

it('dispatches port messages to addEventListener("message") with origin ""', () => {
const sandbox = makeSandbox();
const port = capturePort(sandbox, 'vp_capture');

expect(port).not.toBeNull();
expect(typeof port.addEventListener).toBe('function');
expect(typeof port.removeEventListener).toBe('function');

port.start();
const events: any[] = [];
const listener = (e: any) => events.push(e);
port.addEventListener('message', listener);
// Duplicate add is deduped.
port.addEventListener('message', listener);

sandbox.__boltBridgeReceive(
JSON.stringify({
__boltBridge: true,
type: 'portMessage',
virtualPortId: 'vp_capture',
data: 'payload-1',
})
);

expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({ data: 'payload-1', origin: '' });

port.removeEventListener('message', listener);
sandbox.__boltBridgeReceive(
JSON.stringify({
__boltBridge: true,
type: 'portMessage',
virtualPortId: 'vp_capture',
data: 'payload-2',
})
);
expect(events).toHaveLength(1);
});

it('addEventListener ignores non-message types and non-functions', () => {
const sandbox = makeSandbox();
const port = capturePort(sandbox, 'vp_filter');
port.start();

expect(() => port.addEventListener('not-message', () => {})).not.toThrow();
expect(() => port.addEventListener('message', null)).not.toThrow();
expect(() => port.addEventListener('message', 'not-a-fn')).not.toThrow();

const real = jest.fn();
port.addEventListener('message', real);
sandbox.__boltBridgeReceive(
JSON.stringify({
__boltBridge: true,
type: 'portMessage',
virtualPortId: 'vp_filter',
data: 'x',
})
);
expect(real).toHaveBeenCalledTimes(1);
});

it('a throwing listener does not block sibling listeners', () => {
const sandbox = makeSandbox();
const port = capturePort(sandbox, 'vp_throw');
port.start();

const sibling = jest.fn();
port.addEventListener('message', () => {
throw new Error('boom');
});
port.addEventListener('message', sibling);

sandbox.__boltBridgeReceive(
JSON.stringify({
__boltBridge: true,
type: 'portMessage',
virtualPortId: 'vp_throw',
data: 'x',
})
);

expect(sibling).toHaveBeenCalledTimes(1);
expect(sandbox.console.error).toHaveBeenCalled();
});
});

describe('parseBoltMessage', () => {
it('should parse a JSON string', () => {
const result = parseBoltMessage('{"type":"Focus"}');
Expand Down
Loading
Loading